-
webpack如何诞生?
- github: @sokra Tobias Java开发 GWT(Google Web Toolkit) 作者很喜欢这个功能
- 提供pull request, 将自己的修改提供到别人的修改中, 别人并没有接受
- Instagram 包容了weboack 由Instagram维护
-
为什么需要构建
- 开发分工变化
- 以往
- 前端页面html, 动画效果
- 后台服务端脚本语言, 根据前端页面自己渲染数据, 以及交互逻辑
- 现在
- 前端路由, 单页面应用
- 通过浏览器history/hash实现
- 关注页面逻辑, 数据请求以及逻辑
- 前端自己渲染
- 在业务代码中文件越来越多 (数据层, 视图层, 控制层)
- 复杂度的增加就导致我们需要打包构建
- 以往
- 框架的变化
- JS库(不同浏览器的兼容问题, 原生方法补全) 前端库时代: dojo yui jquery prototype kissy Mootools
- dom操作 ajax请求, 工具方法补充
- 关注度的转换 -> MVC
- backbone.js 前端路由
- undescorejs require.js 模块化的思想开始出现,开始分层
- -> Mv* 关注上层业务开发
- angular
- react
- vue
- 导致构建的出现
- 早期是直接在html页面中使用
- 单页面/服务端喧嚷 推崇模块化,
- 自定义标签, es6新语法,typescript 都需要在构建的过程中进行编译
- JS库(不同浏览器的兼容问题, 原生方法补全) 前端库时代: dojo yui jquery prototype kissy Mootools
- 语言的变化
- HTML的发展史
- css的发展史 -> css3到现在还在更新 -> sass2007 less2009 postcss stylus2010 (css预处理工具)
- 前端脚本语言
- JsScript VBScript ActionScript
- CofffeScript ruby
- ES2017 TS 需要使用编译工具, 才能在浏览器上运行
- 环境的变化
- 在浏览器上运行, 本地创建html文件, 创建一个js, 嵌入在html中
- 前端js代码可以跑在服务端 node
- 移动端: RN PhoneGap H5+CSS3+JS
- 一份代码跑在三端, 服务+浏览器+移动
- 运行环境的变化造成应用复杂度的加大
- 社区的变化
- github
- npm
- 对库的管理更加方便, 开发方式的变化
- 工具的变化
- apache ant java
- 随着nodejs的发展 社区的发展
- grunt fis gulp
- webpack rollup
- 需要构建的原因
- 开发复杂化
- 框架去中心化
- 所有用到的代码中的模块或者功能, 都能使用npm安装
- 库只关注与一个功能解决一个问题, 只安装这一个代码包, 而不是大而全面的解决所有问题的
- 这就意味着包越来越零散, 零散的时候就需要打包
- 语言编译化
- 开发模块化
- 为什么webpack
- vue-cli create-react-app angular-cli 三者都是用webpack作为构建工具
- 支持代码分割
- 天生支持模块化, 将任何文件当做模块
- 开发分工变化
-
基础知识
- 前端发展历史
- 模块化开发
-
开发环境
- 配置sourceMap调试
- 配置远程接口代理
- 配置动态entry更新
- 配置模块热更新
- 配置eslint检查代码格式
-
文件处理
- 编译es6/7
- 编译typescript
- 编译less/sass
- postcss处理浏览器前缀
- 自动生成html模板文件
- 图片压缩和Base64编码
- 自动生成雪碧图
-
打包优化
- 代码分割和懒加载
- 提取公用代码
- Tree-Shaking
- 长缓存配置
-
框架配合
- vue-cli
- angular-cli
- create-react-app
-
收获
- 了解现代前端工程搭建和配置
- 了解现代前端优化手段
- 熟悉通过工具提高开发效率
- 掌握常见的webpack基本配置
所有的代码, 在主流的开发模式中, 都是由一个个的模块构成的. 所有的模块都是可以通过包管理器去拉取我们需要的包; 如同工厂组装手机, 由一个个组件组装成为一个完整的手机
-
js模块化
-
早起使用命名空间
- 库名.类别名.方法名
var NameSpace ={} NameSpace.type = NameSpace.type || {} NameSpace.type.method = function () {}
- 避免重复覆盖的问题
- 命名空间被覆盖
- 使用的时候必须记住完整的路径变量名
- yui2 中大量使用
- 必须团队约定使用命名空间, 公共命名空间
- YUI3沙箱机制 (定义模块 -> 使用模块 -> 指定模块路径)
- 库名.类别名.方法名
-
COMMONJS (node社区) 只能应用在服务端
- 一个文件为一个模块
- 通过module.exports(export)暴露模块接口
- 通过require引入模块 (在服务端 是同步执行的)
const webpack = require('webpack') const mixin = require('./request') const router = require('./router') const router = require('./response') exports = modules.exports = createApplication
-
AMD/CMD(国内)/UMD 帮助我们规范模块化开发的
-
AMD (Async Module Definition)
- 使用define定义模块
- 使用require来加载模块
- requirejs 依赖前置, 提前执行
define( // 模块名 "module-name", // 依赖 ['require', 'exports', 'beta'] // 模块输出 function(require, exports, beta) { // 参数表示依赖模块输出属性 exports.verb = function() { return verb; // Or: return require('beta').verb; } } ) // 支持模块名省略 define( ['a','b','c','d','e'], // 在前面申明并且初始化了所有要用到的模块 function(a,b,c,d,e) { if (false) { // 即便从来没有用到模块b 但是b还是提前执行了 b.foo() } } )
-
CMD (Common Module Definition) 通用模块定义
- 一个文件作为一个模块
- 使用define来定义一个模块
- 使用require来加载一个模块
- SeaJs (尽可能的懒执行) 即便require了, 但是并不会被执行, 直到代码逻辑执行到了才去执行
// 所有的模块定义都通过define来 定义 define(function(require, exports,module) { // 通过require来引入 var $ =require('jquery') var Spinning = require('./spining') // 通过exports提供对外接口 exports.doSomething = function() {} // 或者通过 module.exports提供整个接口 module.exports = { doSomething:function() {} } })
-
UMD打包规范(Universal Module Definition) 通用模块定义
- 通用解决方案
- 三个步骤
- 判断是否支持AMD
- 判断是否支持CommonJS
- 如果都没有, 使用全局变量
(function(root, factory) { if (typeof define==='function' && define.amd) { // AMD支持环境 define([], factory) } else if (typeof exports === 'object') { // CommonJS规范 module.exports = factory(); } else { // 浏览器全局环境 root = window root.returnExports = facotry; } })(this, function(){ return {} })
-
-
ES6 module es的规范普及, 越来越多的使用es6的模块化规范, 但是和common规范有些冲突, 产出通用的规范讨论还在继续
-
Ecmascript Module 一个文件一个模块
-
import/export (default)
import theDefault, {named1, named2 } from 'src/lib' import {named1 as myname1, named2 } from 'src/lib' import * as mylib from 'src/lib' export default 123 export default function() {} export const a = 1; //ovcf const myConst = '' export {myConst as theConst} export { foo, bar} from './src/otherModule'
-
-
上面就是前段模块的一个进步的过程, webpack支持
- AMD (requireJS)
- ES module(推荐使用)
- CommonJS (node)
-
-
css模块化
- css设计模式
- OOCSS 面向对象的css (获得在不同地方使用的css类)
- 结构和设计的分离
- 容器和内容的分离
- SMACSS
- 可扩展和模块化结构
- 目标: 减少代码量, 降低代码的维护难度
- Base 基础样式
- Layout 布局规则 全局
- Module 模块内部
- State 状态
- Theme 主题
- AtomicCSS
- 每一个class都代表自己的独特的意义
- div.mt-10.w-100.h-15
- MCSS (multilayer) 多层级css
- foundation
- base
- project
- cosmetic 装束
- AMCSS 针对属性进行编码设计
- div[amz-size=large am-disabled]
- BEM (Block Element Modifier)
- Block (header, container, menu, checkbox, input)
- Element (menu-item, list-item, checkbox, caption, head-title)
- Modifier: (disabled, highligted, checked, actived, size-big, color-yellow)
- 使用: button.button.button-state-success.big.yellow
- OOCSS 面向对象的css (获得在不同地方使用的css类)
- CSS modules
- css设计模式
-
环境准备
- 命令行工具
- Node+npm
- webpack
-
webpack 简介
- webpack概述
- 将浏览器能够识别的资源文件打包成为static assets
- 官网 webpack.js.org doc.webpack-china.org
- webpack版本迭代
- webpack v1.0.0 -> 2014.02.20
- webpack v2.2.0 -> 2017.1.18
- webpack v3.0.0 -> 2017.06.19
- webpack v4.0.0 beta -> 2018.02
- webpack功能进化
- v1
- 编译, 打包
- HMR 模块热更新
- 代码分割
- 文件处理(loader, plugin)
- v2
- tree shaking 打包以后的代码体积更小, 没有将我们没有用到的代码全部都给打包进来
- ES module v1不支持es6语法, 需要babel支持; v2不需要依赖任何第三方模块
- 动态import import函数
- 新的文档
- v3
- Score Hosting 作用域提升
- 打包以后代码性能的提升, 不是打包性能的提升
- 老版本将每一个模块包裹在一个单独的函数闭包中实现模块系统, 封装的函数系统使得浏览器运行javascript性能有所下降, 闭包越多对浏览器性能损失越大, 学习rollup -> 将所有模块的代码作用域, 提到单一的一个闭包中, 这样保证浏览器运行代码的单一速度, 性能提升体现在打包后代码的运行速度上
- Magic Comments (配合动态import使用)
- 指定webpack懒加载, 指定打包以后的chunk名称
- Score Hosting 作用域提升
- 版本迁移
- v1->v2
- v2->v3 向前兼容
- v3->v4
- 参与社区投票 -> 下一个版本功能 todo list core team
- v1
- webpack概述
-
webpack 核心概念
-
Entry 打包的入口, 代码的入口
-
告诉webpack, 这里面所有的依赖import, require了哪些包, 找到依赖的模块(直接|间接(循环))
-
单个或者多个entry
- 多个入口:
- 多页面应用程序
- 单页面
- 业务代码放到一个entry中
- 框架代码放到另外一个entry中
module.exports = { entry: 'index.js', //entry: ['index.js', 'vendor.js'] // entry: {index: 'index.js', vendor: 'vendor.js'} // entry: {index: ['index.js', 'app.js'], vendor: 'vendor.js'} /* index表示一个key 独特的chunk 代码块 对象的时候好处: 1. 知道每一个entry的chunk是什么 2. 如果想要继续添加入口, 再指定一个key, 清楚知道每一个文件对应的key是什么 */ }
- 多个入口:
-
-
Output 表示输出
- 打包生成的文件的一个描述bundle
- 一个或者多个
- 自定义规则
output: { filename: '[name].bundle.[hash:5].js' } /** name: 表示entry的key值 hash: 表示md5码 表示文件的内容的hash */
-
Loaders 可以处理其他类型的文件, 靠的就是loaders
- 处理文件的
- 将文件转化为模块 (即转化成为js可以处理的)
rules: [{ test: /\.css$/, use: 'css-loader' }]
- 编译相关loader: babel-loader, ts-loader
- 样式相关: style-loader, css-loader, scss-loader, less-loader, postcss-loader
- 文件相关: file-loader, url-loader
-
Plugins 压缩代码, 混淆代码, 代码分割, TreeShaking, 通过自身强大的插件系统实现的
-
参与到打包的过程
-
打包优化和压缩
-
配置编译时的变量
-
极为灵活
module.exports = function() { plugins: [ // 打包过程使用插件 压缩代码 new webpack.optimize.UglifyJsPlugin() ] }
-
常用的插件
- 优化相关:
- CommonsChunksPlugin 提取不同的chunk之间但是相同的代码, 提取单独的一个chunk出来
- UglifyJsWebpackPlugin 混淆压缩代码, 生成js的sourceMap
- 功能相关
- ExtractTextWevbpackPlugin 将css样式提取出来作为一个单独的文件, js引用将css放入到了style标签上
- HtmlWebpackPlugin 帮助生成html文件
- HotModuleReplacePlugin 模块热更新
- CopyWebpackPlugin 帮助我们拷贝文件(打包的时候引用第三方的资源, 不用打包第三方的库,因为引用的第三方资源可能是已经打包好的, 将项目中已经打包好的第三方资源直接移动到我们的输出目录下)
- 优化相关:
-
-
Chunk 代码块
- 默认将代码分成一个个的代码块, 动态懒加载的被分为一个代码块, 甚至使用提取公共代码的, 相同的引用代码就会被独立抽离成为代码块
-
Bundle 打包后的块
-
Module 模块
- loaders将一个个的文件转化为我们的模块, 图片处理完成之后就是一个模块, css文件处理完成之后就是一个模块
-
打包疑问解析
// app.js import sum from './sum' sum(1,2) // app1.js import sum from './sum' sum(2,3) // webpack.config.js entry: { app: './app.js', app1: './app1.js' } output: { filename: '[name].js' } // 1. 结果: 最后打包出来的app1.js 和 app2.js中都会将sum包含其中, 造成重复 output: { filename: 'bundle.js' } // 2. 结果: 最后打包出来的bundle.js 中只会包含一遍sum在其中, 没有重复, 同时控制台给出警告 Conflict: Multiple assets emit to the same filename bundle.js // 3. 在app1.js中 使用amd的方式引入amd定义模块multi require(['./multi'], multi => { const sum = require('./sum') console.log('multi(2,3)', multi(2, 3)) }) // webpack app1.js --output-path=dist // 结果: 会形成一个0.js的打包文件, 因为是异步打包, 单独的chunk加载进来 // 如果在multi.js 中 在define回调中使用require('./sum')的话, sum.js 不会被打包进入0.js中 // 对于AMD的规范, 这类不细讨论
-
-
使用webpack
-
webpack命令使用
- webpack --help (webpack -h) 查看所有的命令
- webpack-v
- 打包命令 webpack <entry> [<entry>] <output> 不适用webpack配置文件的时候
- 使用webpack配置文件, 直接webpack
- --config 指定配置文件 (默认配置文件名称 webpack.config.js或者 webpackfile.js)
- Basic Options
- --entry 指定入口文件
- --watch -w 检测文件系统的变化
- --debug -d 打开调试状态
- --devtool 生成sourceMap
- --progress 进度条显示
- -d 简写 --mode development
- Module Options
- Output Options
- Advanced Options 高级选项
- Resolved Options 解析选项
- Optimization Options 优化选项
- Stats Option 状态选项 (打包出来样式的选项)
-
使用webpack配置(配合node npm使用)
- 不同的配置文件, 开发环境, 生产环境, 测试环境, 打包给第三方使用的
-
第三方的脚手架vue-cli
-
交互式的初始化一个项目
-
项目迁移v1->v2
# wepbpack-cli的使用 webpack-cli init webpack-addons-demo # 项目迁移 webpack-cli migrate <config> ## 只会升级配置文件, package.json里面的文件 需要手机升级
-
-
-
直接使用webpack命名, 使用默认文件或者默认配置
// app.js import sum from './sum' conosle.log(sum(1,2)) // sum.js export default function sum(a, b) { return a + b; } // 打包命令: webpack app.js --output-path=dist --output-filename=bundle.js --mode development // 指定配置文件 webpack --config webpack.config.dev.js
-
编译ES6/7
-
babel-loader
## 安装最新版本loader npm install [email protected] @babel/core --save-dev ## 安装最新preset npm install @babel/preset-env --save-dev
-
npm install babel-loader babel-core --save-dev
-
npm install babel-preset-env --save-dev 指定规范的版本, 只是针对语法
- es2015
- es2016
- es2017
- env 包括2015~2017, 以及latest 用的比较多
- 业内自定义的babel-preset-react
- babel-preset-stage 0 ~3 表示规范组还没有正式发布阶段的
-
babel-presets - options - target 当需要编译的时候, 会根据指定的target来选择那些语法进行编译, 那些语法不进行编译
-
target.browsers 指定浏览器环境
-
target.browsers: 'last 2 versions' 主流浏览器的最后两个版本
-
target.browsers: '> 1%' 大于全球浏览器占有率1%的浏览器
-
数据来源是 browserlist中, can i use中
{ test: /\.js$/, use: { // use: 'babel-loader' //可以直接是一个字符串 loader: 'babel-loader', options: { // 指定preset presets: [['env', { // 告诉babel, 当需要编译的时候, 会根据指定的target来选择那些语法进行编译, 那些语法不进行编译 targets: { browsers: ['> 1%', 'last 2 versions'], // chrome: '52' // 一些新语法浏览器直接支持 不会被转换 } }]] } }, exclude: '/node-modules/' } // 当同时指定'> 1%', 'last 2 versions'的时候, 箭头函数会被转化, const, let等被转化, set不会被转化, num**2 转成了Math.pow // 将targets换成 chrome: '52', 转化后代码基本和原生代码一样
-
-
target.node 指定node环境
-
-
babel-polyfill插件和babel-runtime-transform插件
- 针对一些方法比如数组的map, includes, Set并没有被babel处理, 但是在一些低版本的浏览器中这些方法并没有被实现, 所以需要借助这两个插件
- babel-preset 只是针对语法, 而这两个插件针对函数和方法
- generator
- Set
- Map
- Array.from
- Array.prototype.includes
- 上述方法都没有被babel处理, 所以就需要借助babel的插件进行处理
-
babel-polyfill 垫片, 浏览器之间标准实现的方式不一样,保持浏览器之间同样的API
- 全局垫片 (只要引入, 在全局范围内整个浏览器范围内, 可以对实现的API进行调用)
- 相当于对全局变量的一个污染, 为开发应用而准备的 (在业务中使用, 而不是框架比如vue)
- 使用: npm install babel-polyfill --save 真实项目中的依赖 所以是--save
- 在项目中使用 import 'babel-polyfill'
-
babel-runtime-transform
- 局部垫片
- 为开发框架而准备的, 不会污染全局变量, 会在局部的方法里面新增加变量和方法
- 优势: 当在代码中使用它的时候, 项目中的其他函数,如果使用es6/7方法, 会将每一个引用到的方法打包到单独的文件中去的; 如果使用了runtime-transform, 将其作为一个独立的整体单独打包进去, 相当于文件之间多余的代码就不会再有了
- npm install babel-plugin-transform-runtime --save-dev
- npm install babel-runtime --save
-
.babelrc 在里面配置和babel插件相关的内容
// app.js import sum from './sum' const func = () => { console.log('hello babel') } func() const arr = [1, 2, 3, 4, 5, 4, 3, 2, 1] const arrb = arr.map(item => item * 2) // 下面的语句 不会经过runtime编译 arr.includes(5); // 会经过runtime编译 但是没有exports 使用的时候报错 console.log('SetB', new Set(arrb)) /* function* gen() { yield 1 } */ sum(1, 2) // .babelrc { "presets": [["env", { "targets": { "browsers": ["> 1%", "last 2 versions"] } }]], "plugins": [ "transform-runtime" ] } // 1. 当plugins为空的时候, 上面的代码会完整运行, 都不会被转义 // 2. 添加generator函数的时候, 会报错找不到 regenerator // 3. 添插件的时候 includes不会编译, Set, generator会编译, 但是报错$export is not a function // 4. 屏蔽插件plugins, 使用polyfill, 完美运行所有新属性, 但是打包文件很大, 达到了471Kb
-
实际开发中如何选择
- 如果是应用开发, 只需要配置preset, 如果要使用es6/7新语法, 使用polyfill
- 如果是开发UI库, 框架, 使用runtime
-
-
编译TypeScript
-
JS的超集 tslang.cn 来自于微软
-
官方推荐: npm install typescript ts-loader --save-dev
-
第三方loader: npm install typescript awesome-typescript-loader --save-dev
-
配置: tsconfig.json
## 常用配置选项 compilerOptions:告诉编译器常用的配置选项, 比如 允许js 模块化方式指定:commonjs 指定输出路径等 compilerOptions.module: 模块化方式指定 compilerOptions.target: 编译之后的文件在什么环境下运行的 (类似将语言编译到什么程度) compilerOptions.typeRoots: [ "./node_modules/@type", // 默认安装npm install @types/lodash时路径 "./typings/modules", // 使用typings安装的声明文件路径 ] 指定types声明文件所在的地址 include: 给出一系列的文件路径, 表示需要编译的文件 exclude: 忽略的文件 allowJs: 是否允许js的语法
-
安装声明文件.这样在编译的时候就会给出警告错误, 告诉我们传递的参数类型有错误
- npm install @types/lodash
- npm install @types/vue
- 或者使用typings安装types声明文件, 使用compilerOptions.typeRoots
-
-
提取公用代码
- 减少冗余代码(每一个页面都会存在公共代码, 造成带宽浪费)
- 提高用户的加载速度(只加载页面所需要的依赖, 其他页面在加载的时候, 公共代码已经加载好了)
- CommonChunkPlugin (webpack.optimize.CommonChunkPlugin)
// 针对webpack3 { plugins: [ new webpack.optimize.CommonChunkPlugin({ name: String | Array, // 表示chunk的名称 ? filename: String, // 公用代码打包的文件名称 minChunks: Number|function|Infinity // 数字表示为需要提取的公用代码出现的次数(最小是多少, 比如出现两次以上就提取到公用代码), Infinity 表示不讲任何的模块打包进去, 函数的话表示自定义逻辑 chunks: 表示指定提取代码的范围, 需要在哪几个代码快中提取公用代码 children: 是不是在entry的子模块中 还是在所有模块中查找依赖 deepChildren async: 创建一个异步的公共代码流 }) ] } // webpack4 optimization: { splitChunks: { chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者), minSize: 0, // 30000, // 大于30K会被抽离到公共模块 minChunks: 2, // 模块出现一次就会被抽离到公共模块中, 如果是1的话, 表示将所有的模块都提走, 针对pageA中, 如果只有自己引用jQuery, 那么会生成jQuery-vendor.js 的打包文件 maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个, maxInitialRequests: 3, // 入口模块最多只能加载3个 name: true } }
-
场景
- 单页应用
- 单页应用 + 第三方依赖
- 多页应用 + 第三方依赖 + webpack生成代码 (webpack内置函数)
-
针对单入口的commonChunksPlugin = 并没有将公共部分打包, 只有针对多入口才会
-
多入口文件的时候
entry: { pageA: path.resolve(__dirname, 'src/cmp', 'pageA'), pageB: path.resolve(__dirname, 'src/cmp', 'pageB') // vendor: ['lodash'] }, // webpack3 plugins: [ new webpack.optimize.CommonPluginsChun({ name: 'vendor', minChunks: Infinity }) // 公共模块打包的名字为vendor, entry中也有vendor, 所以会将webpack生成代码以及lodash打包进vendor中 ] // webpack4 splitChunks: { chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者), minSize: 30000, // 大于30K会被抽离到公共模块 // minChunks: 2, // 模块出现两次次就会被抽离到公共模块中 minChunks: Infinity, // 不需要在任何的地方重复 maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个, maxInitialRequests: 3, // 入口模块最多只能加载3个 // name: 'common' // 打包出来公共模块的名称 name: 'vendor' // 打包出来公共模块的名称 } // 1. 会将pageA, pageB中 公共使用的模块打包成进common.chunk.js (name:'common'的时候), 公共模块中包括webpack生成的代码 // 2. lodash只在pageA中使用, 次数为1, 但是minChunks: 2, 所以lodash只会被打包进pageA中 // 3. 在entry中添加 vendor: ['lodash'] 将公共库lodash单独打包, 在webpack4中将其打包进了公共common.chunk中, vendor中只有对lodash的引用 // 4. 如果想将lodash和webpack运行生成时代码以及公共代码打包到一起, minChunks改成Infinity, name:vendor, 将所有生成的文件引用都放到vendor中了 // 5. 保持第三方代码的纯净, 即将第三方代码和webpack分离开, webapck3添加plugins, webpack4添加runtimeChunk配置 // webpack3 new webpack.optimize.CommonPluginsChun({ name: 'manifest', minChunks: Infinity }) // 发现vendor和manifest处大部分代码是一样的可以, 可以改成 new webpack.optimize.CommonPluginsChun({ names: ['vendor','manifest'], minChunks: Infinity }) // webpack4 runtimeChunk: { name: 'manifest' }, // 结果是: 将webpack生成的代码打包到manifest中, 将lodash打包进vendor中, 将引用次数超过两次的打包进vendor中
-
代码分割和懒加载
- 通过代码分割和懒加载, 让用户在尽可能的下载时间内加载想要的页面, 只看一个页面的时候, 下载所有的代码, 带宽浪费;
- 在webpack中, 代码分割和懒加载是一个概念, webpack会自动分割代码, 然后再把需要的代码加载进来, 不是通过配置来实现的, 通过改变写代码的方式来实现的, 当依赖一个模块的时候, 告诉webpack我们是需要懒加载或者代码切分, 通过两种方式来实现
- webpack.methods
- require.ensure() 接收四个参数
- 第一个参数dependencies, 加载进来的代码并不会执行, 在callback中引入, 这个时候才会去执行, 第三个参数errorBack, 第四个参数chunkName
- 如果浏览器不支持promise, 需要添加垫片
- require.include 只有一个参数, 只引入进来, 但不执行
- 当两个子模块都引入了第三个模块, 可以将第三个模块放入父模块中, 这样动态加载子模块的时候, 父模块已经有了第三方模块, 不会在多余加载; 比如subPageA, subPageB都引入了moduleA, 但是moduleA不会被打包进父依赖, 所以可以使用include
- ES2015 loader spec (动态import) stage-3
- 早起system.import
- 后来import方式 返回一个Promise
- import().then
- webpack import function 通过注释的方式来解决动态的chunkName以及加载模式
import( /*webpackChunkName: async-chunk-name*/ /*webpackMode: lazy*/ moduleName )
- webpack.methods
- 代码分割的场景
- 分离业务代码和第三方依赖 (提取公共代码中有涉及)
- 分离业务代码 和 业务公共代码 和 第三方依赖; 相比于上一个,将业务代码拆成两部分
- 分离首次加载 和 访问后加载的代码 (访问速度优化相关的) - LazyLoad - 提高首屏加载速度
// 0. 单入口pageA, 不做任何的优化 直接引入 subPageA, subPageB, lodash 会发现pageA非常大 // 1. 异步引入, 将lodash打包到vendor中 require.ensure('lodash', require => { const _ = require('lodash') _.join([1, 2, 3], 4) console.log(_) }, 'vendor') // 2. pageA.js中修改 if (page === 'subPageA') { // require([]) 参数是空数组的话, 里面的require的包还是会被异步打包 require.ensure(['./subPageA'], require => { // 如果不require的话, 那么就不会执行subPageA中的代码块 const subPageA = require('./subPageA') console.log(subPageA) }, 'subPageA') } else if (page === 'subPageB') { require.ensure(['./subPageB'], require => { const subPageB = require('./subPageB') console.log(subPageB) }, 'subPageB') } // 结果: moduleA分别在打包好的文件 subPageA.chunk.js 和 subPageB.chunk.js中, 公共部分moduleA没有被提取出来 // 3. 单entry有上述公共代码的情况的话, 使用inlcude的情况处理, 将module在父模块pageA.js提前引入, 但是并不运行 require.include('./moduleA') // 结果: moduleA被打包进入了pageA.bundle.js中, 这样就完成了代码分割 // --- import 方案 ------------- /* 坑: import 只有在stage-0 或者 syntax-dynamic-import yarn add babel-preset-stage-0 babel-plugin-syntax-dynamic-import --dev .babelrc { "presets": ["stage-0"], "plugins": ["syntax-dynamic-import"] } 上述两种情况只使用一种即可 */ // 在import的时候 代码实际上已经执行了 if (page) { import( /* webpackChunkName: "subPageA" */ /* webpackMode: "lazy" */ './subPageC' ).then(subPageC => { console.log(subPageC) }) } else { import( /* webpackChunkName: 'subPageD' */ /* webpackMode: "lazy" */ './subPageD' ) }
-
async 在代码分割中如何使用, 即结合commonChunkPlugin
// webpack.plugin.lazy.cmp.js entry: { pageA: path.resolve(__dirname, 'src/lazy_cmp', 'pageA'), pageB: path.resolve(__dirname, 'src/lazy', 'pageB'), vendor: ['lodash'] } // webpack3 plugins: [ new wepback.optimize.CommonsChunkPlugin({ // async 指定为true表示异步模块, 或者指定为 异步模块提取后的名称 async: 'async-common', children: true, // 表示不仅仅是两个入口页面之间, 而且还是两个页面之间的子依赖中去寻找 minChunks: 2 }), new wepback.optimize.CommonsChunkPlugin({ // lodash打包进入vendor中, manifest是webpack运行时代码 names: ['vendor', 'manifest'], minChunks: Infinity }) ] // webpack4 optimization: { // webpack runtime 代码 runtimeChunk: { name: 'manifest' }, // 公共模块提取 splitChunks: { chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者), minSize: 30000, // 大于30K会被抽离到公共模块 // minChunks: 2, // 模块出现两次次就会被抽离到公共模块中 minChunks: Infinity, // 不需要在任何的地方重复 maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个, maxInitialRequests: 3, // 入口模块最多只能加载3个 name: 'vendor' // 打包出来公共模块的名称 } } // pageA.js import _ from 'lodash' / 1. 这里不再使用include, 因为会和pageA打包到一起, 这里的目的是 将其异步单独提取出来 // require.include('./moduleA') const page = 'subPageA' // 在pageB中, 这里page='subPageB', 其余一样 if (page) { import( /* webpackChunkName: "subPageA" */ /* webpackMode: "lazy" */ './subPageA' ).then(subPageA => { console.log(subPageA) }) } else { import( /* webpackChunkName: 'subPageB' */ /* webpackMode: "lazy" */ './subPageB' ) } // 2. webpack3 结果: 将异步打包结果中subPageA和subPageB中的公共模块moduleA, 单独的提取到了async-common-pageA.chunk.js中 这里比较坑的困惑: commonsChunkPlugin参数说的不是很明确, 比如async, children, deepChildren, minChunk, 他们之间是有依赖忽视关系的 // 3. webpack4 结果: chunks:all, 结果是将多次引用的公共模块moduleA, lodash提取到了vendor.chunk中, 其余的和webpack3一样, 生成打包文件pageA.chunk, pageB.chunk(入口文件), subPageA.chunk, subPageB.chunk(异步单独提取), manifest.chunk(webpack-runtime单独提取)
-
处理CSS
-
每一个模块都有自己的css文件, 在使用的时候将css样式引入
-
如何在webpack中引入css
-
style-loader 在页面中创建style标签, 标签里面的内容就是css内容
- style-loader/url
- style-loader/useable
-
css-loader 如何让js可以import一个css文件, 包装一层, 让js可以引入css文件
// index.js import './css/base.css' // webpack.config.style.js { test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader' } ] } // 将打包后的文件引入到index.html中 // 1. 结果: 在html中生成了style标签, 将base.css标签中的样式放到了style标签中 // 2. 生成link标签的形式 (不过用的比较少) 注意publicPath配置 use: [ { loader: 'style-loader/url' // loader: 'style-loader/useable' }, { loader: 'file-loader' } ] // 结果: style-loader/url 单独生成一份css文件 , 但是引入多个文件的时候, 会生成多个link标签, 会造成越多的网路请求 //3. style-loader/useable import base from 'base.css' import common from 'common.css' var flag = false; setInterval(function() { if(flag) { base.use() } else { base.ununse() } flag = !flag; }, 2000) // base.use() 样式插入到style标签中 // common.unuse() // 控制样式不被引用 // 结果: 没过2000ms, 页面中样式循环引用和删除
-
StyleLoader 配置
-
insertAt (插入位置)
-
insertInto(插入到DOM)
-
singleton (是否只使用一个style标签) 当css模块比较多的时候 会有很多css标签
-
transform (转化, 浏览器环境下, 插入页面之前)
transform: './src/style/css.transform.js' // css.transform.js 文件内容 // 该函数并不是在打包的时候执行的,在运行webpack的时候, 是不行执行的 // 在style-loader 将样式插入到DOM中的时候 执行的, 运行的环境是在浏览器环境下, 可以拿到浏览器参数, window,UA // 可以根据当前浏览器来对当前的css进行形变 module.exports = function(css) { console.log(css) console.log(window.innerWidth) // 输出形变以后的css if (window.innerWidth >= 768) { css = css.replace('yellow', 'dodgerblue') } else { css = css.replace('yellow', 'orange') } return css; }
- 针对每一次在index.js中引入的css文件都会执行上面的代码
-
-
CssLoader 配置参数
- alias 解析的别名 将引入css的路径映射到其他地方
- importLoader 取决于css处理后面是不是还有其他的loader (sass会使用到 @import)
- minimize 是否压缩
- modules 是否启用css-modules
- 打包出来的样式class 都变成一段随机字符串
-
-
CSS modules
-
:local 给定一个本地的样式 局部的样式
-
:global 给定一个全局样式
-
compose 继承一个样式
-
compose ... from path 引入一个样式 (尽量将composes放在前面, 这样可以控制引入顺序, 样式不会被覆盖)
// base.css .box { composes: big-box from './common.css'; height: 200px; width: 100px; border-radius: 4px; background: #696969; }
-
localIdentName: '[[path]][name]_[local]--[hash:base64:5]' 控制生成的class类名
- path代表引用css路径 name表示文件名称 local本地样式名称
-
-
配置less/sass
-
npm install less-loader less --save-dev
-
npm install sass-loader node-sass --save-dev
.header { composes: font from './header.less' }
-
-
提取css代码 - 提取公共代码 做缓存 (不提取的话, 将css代码打包到了js文件中)
-
extract-loader
-
ExtractTextWebpackPlugin
- npm install extract-text-webpack-plugin --save-dev
// webpack3 var ExtractTextWebpackPlugin = require('ExtractTextWebpackPlugin) module: { rules: [ { test: /\.less$/, use: ExtractTextWebpackPlugin.extract({ fallback: { // 告诉webpack, 当不提取的时候, 使用何种方式将其加载到页面中 loader: 'style-loader, options: { singleton: true, // transform: '' } }, use: [ {loader: 'css-loader'} {loader: 'less-loader'} ], // 定义我们继续处理的loader }) } ] }, plugins: [ new ExtractTextWebpackPlugin({ filename: '[name].min.css', // 提取出来的css的名称 // 将css-loader的option中的minimize打开 // allChunks 给插件指定一个范围, 指定提取css的范围 // 1. 设置为true 表示所有的引用的css文件都提取 // 2. 设置为false, 默认, 只会提取初始化的css(异步加载不认为是初始化) allChunks:false, }) ] // webpack3 结果: index.bundle.js app.min.css 但是打开index.html 并没有插入进去 // webpack4 { test: /\.less$/, use: [ MiniCssExtractPlugin.loader, { loader: 'css-loader', // loader: 'file-loader' options: { minimize: process.env.NODE_ENV === 'production', modules: true, localIdentName: '[path]_[name]_[local]--[hash:base64:5]' } }, { loader: 'less-loader' } ] } plugins: [ new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: '[name].css', chunkFilename: '[id].css' }) ]
-
异步引入a.js文件, 在a.js文件中引入a.less
-
针对allChunks为false的情况
- webpack3: 生成a.bundle.js文件, css文件被当成js的一个模块被打包处理, 将css放在js文件里面, 一起被提取; css代码切分的一种方式, 将初始化加载和动态加载区分开; 借助动态加载的代码区分, 也是css-in-js的一个概念
- weboack4: 生成moduleA.chunk.js 和moduleA.chunk.css文件, 在index.bundle.js 包括了对于modulA.js和module.css文件的引用
-
webpack4使用splitChunks配置
optimization: { splitChunks: { cacheGroups: { styles: { name: 'styles', test: /\.scss|css$/, chunks: 'all', // merge all the css chunk to one file enforce: true } } } }
- 结果: 生成index.bundle.js style.chunk.js style.chunk.css 将所有的样式文件都打包进了style.chunk.css文件中, 但是需要手动添加到项目htm中
- question: 为什么这里不会运行? npm run extract
-
-
-
PostCss (Autoprefixer CSS-nano CSS-next)
A tool for transforming Css With Javascript 用js去转化css的一个工具
-
联系到上一节中的css.transform.js, 但是时机是不一样的, PostCss是打包的时期, css.transform是浏览器插入到style标签中的时候
-
postcss的强大, 理解成为一个处理css的工具
-
安装 npm install postcss postcss-loader autoprefixer cssnano postcss-cssnext --save-dev
-
autoprefixer: 帮助加上浏览器前缀
-
css-nano 帮助我们优化压缩css, 在postcss可以当做插件使用, css-loader就是用的css-nano做的压缩
-
css-next 使用未来的css新语法
- css variables
- custom selectors 自定义选择器
- calc() 动态计算 ...
{ loader: 'postcss-loader', options: { // require进来的插件给postcss使用的 ident: 'postcss', // 表明接下来的插件是给postcss使用的 plugins: [ // require('autoprefixer')(), // 两个一起用cssnext 会给出警告, 提示已经包含autoprefixer require('postcss-cssnext')() ] } },
-
-
一旦涉及到浏览器兼容性问题的时候, 一定会有针对的浏览器兼容问题, 使用browserlist, 让所有的插件都公用一份browserlist
- 可以放在package.json里面
- .browserlistrc 存入对浏览器的要求
-
postcss-import 插件 将@import的文件内容直接放入到当前的css文件中, 但是存过来之后要考虑相对文件路径的变化, 需要配合postcss-url来使用
-
postcss-assets 在后面资源处理讲解
-
-
Tree Shaking (摇动树?)
-
webpack2.0新引进, 基本概念:引申到项目, 在项目中如果有代码不再用到, 或者说是从来没有用到过, 那么项目如果在上线的时候, 如果代码中还存在, 势必造成资源的浪费;
-
使用场景
- 常规优化(体积更小, 加载时间更快)
- 引入第三方库的某一个功能(lodash) 只用其中的一两个功能, 但是打包整个包造成了浪费
-
TreeShaking 分为两种
-
针对项目中的js文件, JS TreeShaking (将没有用到的方法给去掉)
- 在webpack2之后 webpack会将没有用到的文件标识出来, 借助插件的帮助webpack.optimize.uglifyJs 将废弃的代码移除掉 webpack4-demo
// webpack 3 plugins: [ // 将没有用到的文件删除 new webpack.optimize.UglifyJsPlugin({}) ] // webpack3 针对lodash /* import func from 'lodash/set' babel-plugin-lodash --save-dev */ // webpack4 optimization: { minimize:true } // 同时需要修改babelrc中添加一层配置, 来开启无用的模块检测, 但是在webpack4中无效 { modules:false } // 主要通过在package.json文件中设置sideEffects: false来告诉编译器该项目或模块是pure的,可以进行无用模块删除。 // 开发环境下, 试过很多次, 都无法进行treeShaking // 生产模式下, 自动识别为TreeShaking
-
针对项目中的css文件 CSS TreeShaking (DOM节点有各种各样的id, class等属性), 有些样式没有被匹配不上, 就不会被打包到样式中去
-
-
-
TreeShaking 浅析
-
webpack 本身并不会删除任何多余的代码,删除无用代码的工作是 Uglify做的。webpack 做的事情是 unused harmony exports 的标记,也就是他会分析你的代码,把不用的 exports 删除,但是他不会删除全部的代码,只是把 exports 关键字删除了,变量的声明并没有删除。
-
上面是一个模块,如果我们这样引用
import { x } from './a.js'
,那么webpack会进行unused harmony exports
分析,他会发现你的y
函数根本没用到,于是把y的exports
声明去掉了,留下了一个无用的y
函数声明。于是输出这样的代码: -
如果我们启用
Uglify
插件,由于y 函数没有被任何人使用,所以 Uglify 会把它直接删掉。 -
简单总结: 这就是webpack 在
tree-shaking
中扮演的角色:他会进行无用导出的分析,并把对应的 export 删除,但变量本身的声明并不会被删除。删除代码的操作是交给 uglify来做的。 -
原理浅析:
-
webpack 的这个特性仅限于
ES6
模块语法,如果你用了 nodejs 的模块,即module.exports
,那么webpack 将 不会做任何优化。所以如果你使用了babel
,请一定不要把 ES6 modules 编译掉,否则 tree-shaking 将没有效果。 -
为什么会这样呢,因为
ES6 Modules
是静态的,而 CMD 是动态的 -
- ES6 Modules 只能作为模块顶层的语句出现,不能出现在 function 里面或是 if 里面。
- import 的模块名只能是字符串常量。
- 不管 import 的语句出现的位置在哪里,在模块初始化的时候所有的 import 都必须已经导入完成。换句话说,ES6 imports are hoisted。
- import binding 是 immutable 的,类似 const。比如说你不能 import { a } from './a' 然后给 a 赋值个其他什么东西。
正因为这些特性,使得静态分析依赖变得很靠谱。否则如果无法静态分析,自然无法在编译阶段进行tree shaking。
-
运行代码: webpack3-commonChunkPlugin
- npm run treeshaking
- 添加uglifyJsPlugin插件, 查看压缩之后的代码
-
-
TreeShaking 处理 ts
-
分析代码:npm run tstreeshaking sayWorld居然还是存在!!!怎么回事,为什么没有被触发tree-shaking优化?
-
因为tsc编译后的代码为es5 ,而正因如此,tsc默认使用了commonJS的规范来加载模块,因此并没有触发tree-shaking
-
-
TreeShaking 副作用
- **TreeShaking注意点:**显而易见的是,webpack 通过静态语法分析,找出了不用的 export ,把他们改成 free variable,而 Uglify同样也通过静态语法分析,找出了不用的变量声明,直接把他们删了。并不是直接流氓式的删除, 这点可以放心
- 但是函数副作用是非常复杂的,有没有可能被删除的
y
函数其实是有副作用的?肯定是存在的,比如我们在x
中引用 一个对象的属性,然后设置getter
,那么读取属性就会报错, 所以这里是我们需要注意的地方 (webpack4-treeshaking代码分析)
-
-
-
webpack只是一个打包模块的机制,只是把依赖的模块转化成可以代表这些包的静态文件。
-
webpack把任何形式的资源都视作模块 - loader机制, 不同的资源采用不同的loader进行转换。
-
CMD、AMD 、import、css 、等都有相应的loader去进行转换。那为什么我们平时写的es6的模块机制,不用增加import的loader呢。因为我们使用了babel把import转换成了require。并且Webpack 2 将增加对 ES6 模块的原生支持并且混用 ES6、AMD 和 CommonJS 模块。这意味着 Webpack 现在可以识别 import 和 export 了,不需要先把它们转换成 CommonJS 模块的格式
-
在解析对于文件,会自动去调用响应的loader**loader 本质上是一个函数,输入参数是一个字符串,输出参数也是一个字符串。当然,输出的参数会被当成是 JS 代码,从而被 esprima 解析成 AST,触发进一步的依赖解析。**webpack会按照从右到左的顺序执行loader
-
-
每次在命令行输入 webpack 后,操作系统都会去调用 ./node_modules/.bin/webpack 这个 shell 脚本。
这个脚本会去调用 ./node_modules/webpack/bin/webpack.js 并追加输入的参数, 如 -p , -w 。(图中 webpack.js 是 webpack 的启动文件,而 $@ 是后缀参数)
-
optimist 分析参数并以键值对的形式把参数对象保存在 optimist.argv 中
-
config 合并与插件加载
- 在加载插件之前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到
./node_modules/webpack/bin/convert-argv.js
中,通过判断 argv 中参数的值决定是否去加载对应插件。(至于 webpack 插件运行机制,在之后的运行机制篇会提到)
- 在加载插件之前,webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置在 webpack.config.js 的 plugins 。接着 optimist.argv 会被传入到
-
-
- 在加载配置文件和 shell 后缀参数申明的插件,并传入构建信息 options 对象后,开始整个 webpack 打包最漫长的一步。而这个时候,真正的 webpack 对象才刚被初始化,具体的初始化逻辑在 lib/webpack.js 中
- webpack 的实际入口是 Compiler 中的 run 方法,run 一旦执行后,就开始了编译和构建流程 ,其中有几个比较关键的 webpack 事件节点。((可以简单的看成是也该生命周期)
-
核心对象Compilation
- compiler.run 后首先会触发 compile ,这一步会构建出 Compilation 对象
- 这个对象有两个作用,一是负责组织整个打包过程,包含了每个构建环节及输出环节所对应的方法,可以从图中看到比较关键的步骤,如 addEntry() , _addModuleChain() , buildModule() , seal() , createChunkAssets() (在每一个节点都会触发 webpack 事件去调用各插件)。二是该对象内部存放着所有 module ,chunk,生成的 asset 以及用来生成最后打包文件的 template 的信息。
-
编译与构建主流程
- 上一步主要是构建出Compilation 对象 , 得到所有的资源信息以及输出对应环节方法
- 在创建 module 之前,Compiler 会触发 make,并调用上一步创建的对象
Compilation.addEntry
方法,通过 options 对象(第一步中收集的option)的 entry 字段找到我们的入口js文件。之后,在 addEntry 中调用私有方法_addModuleChain
,这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。 - 而构建模块作为最耗时的一步,又可细化为三步:
- 调用各 loader 处理模块之间的依赖
- webpack 提供的一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些 loader ,比如
url-loader
,jsx-loader
,css-loader
等等来让我们可以直接在源文件中引用各类资源。webpack 调用doBuild()
,对每一个 require() 用对应的 loader 进行加工,最后生成一个 js module。 - 这里简单理解就是: 将各种类型的文件处理成为js module, 让webpack可以处理
- webpack 提供的一个很大的便利就是能将所有资源都整合成模块,不仅仅是 js 文件。所以需要一些 loader ,比如
- 调用 acorn 解析经 loader 处理后的源文件生成抽象语法树 AST
- A tiny, fast JavaScript parser, written completely in JavaScript.
- 语言解析器来获取整一个 AST(abstract syntax tree)。
- 遍历 AST,构建该模块所依赖的模块
- 对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过
addDependency()
添加到数组中。当前模块构建完成后,webpack 调用processModuleDependencies
开始递归处理依赖的 module,接着就会重复之前的构建步骤。 - 查找依赖项, 理解为: 从入口文件开始, 每一个module依赖了哪些模块, 把这些依赖放在addDependency数组中
- 对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过
-
构建细节
- module 是 webpack 构建的核心实体,也是所有 module 的 父类,它有几种不同子类:
NormalModule
,MultiModule
,ContextModule
,DelegatedModule
等。但这些核心实体都是在构建中都会去调用对应方法,也就是build()
。 build方法: 1. 初始化module信息,如context,id,chunks,dependencies等 2. 构建计算时间等等 - 对于每一个 module ,它都会有这样一个构建方法。它还包括了从构建到输出的一系列的有关 module 生命周期的函数,我们通过 module 父类类图其子类类图(这里以 NormalModule 为例)来观察其真实形态
- 可以看到无论是构建流程,处理依赖流程,包括后面的封装流程都是与 module 密切相关的。
- module 是 webpack 构建的核心实体,也是所有 module 的 父类,它有几种不同子类:
-
1. 在所有模块及其依赖模块 build 完成后,webpack 会监听 `seal` 事件调用各插件对构建后的结果进行封装,要逐次对每个 module 和 chunk 进行整理,生成编译后的源码,合并,拆分,生成 hash 。 同时这是我们在开发时进行代码优化和功能添加的关键环节。 2. 生成最终的assets
- 在封装过程中,webpack 会调用 Compilation 中的
createChunkAssets
方法进行打包后代码的生成。 createChunkAssets 流程如下: - 不同的template: 从上图可以看出通过判断是入口 js 还是需要异步加载的 js 来选择不同的模板对象进行封装,入口 js 会采用 webpack 事件流的 render 事件来触发
Template类
中的renderChunkModules()
(异步加载的 js 会调用 chunkTemplate 中的 render 方法)。 - 在 webpack 中有四个 Template 的子类,分别是
MainTemplate.js
,ChunkTemplate.js
,ModuleTemplate.js
,HotUpdateChunkTemplate.js
,前两者先前已大致有介绍,而 ModuleTemplate 是对所有模块进行一个代码生成,HotUpdateChunkTemplate 是对热替换模块的一个处理。 - 模块封装: 模块在封装的时候和它在构建时一样,都是调用各模块类中的方法。封装通过调用
module.source()
来进行各操作,比如说 require() 的替换, moduleId的替换。 - 生成assets: 各模块进行 doBlock 后,把 module 的最终代码循环添加到 sourcemap 中。一个 sourcemap 对应着一个 asset 对象,该对象保存了单个文件的文件名( name )和最终代码( value )。
- 在封装过程中,webpack 会调用 Compilation 中的
-
所有的模块都是js模块
- webpack 只支持JS模块,所有其他类型的模块,比如图片,css等,都需要通过对应的loader转成JS模块。所以在webpack中无论任何类型的资源,本质上都被当成JS模块处理。
-
所有的loader都是一个管道
-
在webpack中,可以把一个loader看做是一个数据管道,进口是 一个字符串,然后经过加工,输出另一个字符串,多个loader可以像水管一样串联起来,很像java中的stream流。
-
很多人都说webpack复杂,难以理解,很大一部分原因是webpack是基于配置的,可配置项很多,并且每个参数传入的形式多种多样(可以是字符串、数组、对象、函数。。。),文档介绍也比较模糊,这么多的配置项各种排列组合,想想都复杂。而gulp基于流的方式来处理文件,无论从理解上,还是功能上都很容易上手。
-
上面简单对比了webpack与gulp配置的区别,当然这样比较是有问题的,gulp并不能进行模块化的处理。这里主要是想告诉大家使用gulp的时候,我们能明确的知道js文件是先进行babel转译,然后进行压缩混淆,最后输出文件。而webpack对我们来说完全是个黑盒,完全不知道plugins的执行顺序。正是因为这些原因,我们常常在使用webpack时有一些不安,不知道这个配置到底有没有生效,我要按某种方式打包到底该如何配置?
为了解决上面的问题,webpack4引入了
零配置
的概念, 能方便我们不少 -
loader的优点 *2
-
-
最简单的babel-loader如何指定使用自己的loader呢
- 直接在
webpack
config 文件里面加一个resolveLoader
的配置即可,我们这里把bable-loader
指定为自己写的。 - 要实现一个babel-loader, 很显然我们需要使用babel-core来编译代码 (这里就是为什么我们在使用babel-loader的时候, 要安装babel-core babel-loader babel-presets) core的一个原因
- 查一下 babel-core 文档,可以调用
babel.transform
API来编译代码。再加上一些presets
的设置,我们可以把上面的代码做一下改造如下: - Babel-core 的API用法不在这里详细解释,有兴趣的可以直接去看官方的文档。我们这里做了一个很简单的转换,就是把 接收到的
source
源码,用babel
编译一下,然后返回编译后的代码。
- 直接在
-
支持一下sourceMap
- 如果需要支持 sourcemap,显然需要把
babel-core
产生的sourcemap
传给webpack
。之前因为只返回编译后的代码,所以我们直接返回了字符串,如果需要同时返回编译的代码和sourcemap,我们需要这个接口 this.callback,(不要问我怎么知道的) 因为官方文档上是这么说的 - 官方babel-loader 显然他的代码比我这边的代码多很多,他写了那么多,其实主要是增加了 Cache,以及增加了对异常的处理, 优化, 环境适配等
- 如果需要支持 sourcemap,显然需要把
-
支持一下jsx
- 我们是使用React写的组件,那么同样可以通过
babel-loader
来编译。关于如何编译JSX,babel官网这里做了很详细的文档 transform-react-jsx - 简单来说,就是
babel-core
本身虽然不支持jsx
,会报语法错误,但是我们可以通过加载一个插件就能支持 jsx - react 因为用的JSX,而jsx 因为全部编译成了JS 所以它的loader很简单。但是 Vue 的组件并不是可以直接就全部编译成JS,而是包括了 html,JS,CSS三部分,所以
vue-loader
相对来说就复杂很多了。 这里不探讨有兴趣可以下去分析
- 我们是使用React写的组件,那么同样可以通过
-
Style-Loader 和 Css-Loader
- Style-Loader和CSS-Loader的工作原理 CSS代码会先被
css-loader
处理一次,然后再交给style-loader
进行处理。那么这两步分别是做什么呢?- css-loader 的作用是处理css中的
@import
和url
这样的外部资源 - style-loader 的作用是把样式插入到 DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的 innerHTML 里
- css-loader 的作用是处理css中的
- 通过图可以看到一个loader只做一件事。我们对图片等资源的处理和把样式插入到DOM分为两个任务,这样每个loader做的事情就比较简单,而且可以通过不同的组合实现更高级的功能。
- Style-Loader和CSS-Loader的工作原理 CSS代码会先被
-
Style-LOader原理解析
style-loader
的主要作用就是把 CSS 代码插入DOM中- pitch解释
- 正常情况下,我们会用 default 方法,那么这里我们为什么用一个 pitch 方法呢?简单的解释一下就是,默认的loader都是从右向左执行,用 pitching loader 是从左到右执行的。
- 我们为什么用 pitching loader呢?因为我们要把CSS文件的内容插入DOM,所以我们要获取CSS文件的样式。**如果按照默认的从右往左的顺序,我们使用 css-loader, 它返回的结果是一段JS字符串,这样我们就取不到CSS样式了。**为了获取CSS样式,我们会在
style-loader
中直接通过require来获取,这样返回的JS就不是字符串而是一段代码了。也就是我们是先执行style-loader
,在它里面再执行css-loader
。 同样的字符串,但是在默认模式下是当字符串传入的,在pitching模式下是当代码运行的,就是这个区别。 - 也就是,我们处理CSS的时候,其实是 styled-loader先执行了,它里面会调用 css-loader 来拿到CSS的内容,拿到的内容当然是经过css-loader 编译过的。 style-loader此时从css-loader获取到的是JSObject, 可以执行的js对象, 从这个对象中获取到样式
- 需要提的一点是,其实 css-loader 返回的也不是css的内容,而是一个对象,不过他的 toString() 方法会直接返回css的样式内容,那么为什么是这样的呢,因为这个是 css-loader 返回的结果
- addStyle做了什么 可以直接读 style-loader 的源码,其实 addStyle 做的核心的事情就是在head中插入了一个 style标签,并把 CSS 内容写入这个标签中。
-
代码分割个懒加载
-
文件的处理 - 图片处理
-
css中引入的图片 file-loader
-
优化角度: 自动合成雪碧图 postcss-sprites
-
针对retina屏幕的处理 给potcss-sprites 添加配置 retina:true 即可
-
同时修改图片文件 [email protected] 告诉loader是需要处理的
-
同时需要就空间dom设置的宽高缩小一倍
{ loader: 'postcss-loader', options: { // require进来的插件给postcss使用的 ident: 'postcss', // 表明接下来的插件是给postcss使用的 plugins: [ // require('autoprefixer')(), require('postcss-sprites')({ // 合成图片的路径 spritePath: 'dist/filedeal/assets/imgs/sprites/', // retina: true, 让postcss帮助我们处理@2x屏幕的大小 // 相应的位置元素大小必须是原始的一半 }), // 合成精灵图 require('postcss-cssnext')() ] } },
-
-
压缩图片 img-loader
-
Base64编码 url-loader
-
-
文件处理- 字体文件
{ test: /\.(eot|woff2?|woff|ttf|svg)/, use: [ { loader: 'url-loader', options: { limit: 5000, // 大于5k生成文件 useRelativePath: true, name: '[name].[hash:5].min.[ext]' // 打包后文件的名称控制 } } ] }
@font-face { font-family: "iconfont"; src: url('../assets/fonts/iconfont.eot?t=1531054753224'); /* IE9*/ src: url('../assets/fonts/iconfont.eot?t=1531054753224#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('../assets/fonts/iconfont.svg?t=1531054753224#iconfont') format('svg'); /* iOS 4.1- */ } .iconfont { font-family: "iconfont" !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-ke:before { content: "\e604"; } .icon-fudao:before { content: "\e605"; }
-
处理第三方库
-
第三方的库在远程的cdn上
-
在自己的项目下管理(通用的模块, 但是不需要每次都去import)
-
上述可以通过插件的方式实现webpack.providePlugin, 在模块中注入我们需要的变量json, value就是模块的名称, 在使用的时候就是require
// 1. 针对使用线上cdn的情况 // 在plugins数组中 new webpack.ProvidePlugin({ // npm install jquery $: 'jquery' // key表示我们使用的时候的名称 $('div').addClass('new') }) // 使用的时候, 不需要import 直接使用变量即可 $('div').addClass('provide-class') // 2. 针对使用本地文件的形式 new webpack.ProvidePlugin({ // npm install jquery // 这里的模块value必须和上面定义的alias中的key一致 React: 'react', ReactDOM: 'react-dom' }) resolve: { // 告诉webpack, 如果在node_modules中找不到的时候, 去哪里找模块 alias: { // $表示只是将这一个ReactDOM关键字解析到某一个目录的文件下, 而不是解析到一个目录下 react$: path.resolve(__dirname, 'src/filedeal/libs/react.development.js'), 'react-dom$': path.resolve(__dirname, 'src/filedeal/libs/react.dom.development.js') } },
-
imports-loader, 通过options传参, 通过test匹配到模块
- 下面代码的意思就是将 $:jquery 注入到app.js文件当中
{ test: path.resolve(__dirname, 'src/app.js'), use: [ { loader: 'imports-loader', options: { // value会被解析, 从node_modules或者是从alias $: 'jquery' } } ] }
-
window上挂载(比较野蛮, 调试的时候可以)
-
通过ProvidePlugin和 import直接引入区别
-
import $ from 'jquery' 引入之后,无论你在代码中是否使用jquery, 打包后, 都会打进去, 这样其实产生大量的冗余js
-
Provideplugin, 只有你在使用到此库, 才会打包
-
提取第三方库(或者想单独提出来的)js库, 增加一个optimization配置
// 在webpack3.x版本之前:使用new webpack.optimize.CommonsChunkPlugin现在已经不支持 new webpack.optimize.CommonsChunkPlugin({ name:'jquery' })
-
-
-
-
生成html
-
自动生成html(将配对的css, js引入) HtmlWebpackPlugin
- template 模板名称
- filename 指定文件名
- minify 指定生成的html文件是否压缩
- chunks 指定哪些chunk插入到html (多页面程序, a,b页面, b页面就不需要插入a页面的chunk代码 )
- inject script标签插入的位置(body, head, false)
new HtmlWebpackPlugin({ filename: 'index.html', template: './src/html/index.html', // inject: 'body', // 默认脚本插入在body尾部, 样式head尾部 // chunks: [] 不指定chunks会将上面所有打包的chunk嵌入到html中, 针对多页面可以配置该选项 })
-
-
HTML中引入图片 - html-loader
- attrs: [img:src] 每一项表示一个规则, 左边标识标签, 右边是属性; 让webpack来打包
{ test: /\.html$/, use: [ { loader: 'html-loader', // 需要注意的是路径问题 options: { attr: ['img:src', 'img:data-src'] } } ] } <img src="./assets/imgs/banner_2.png" data-src="./assets/imgs/banner_2.png" alt="html-loader 打包"> <!-- 打包后在html中查看是查看不到的--> <img src="${require('./assets/imgs/banner_1.png')}" alt="">
-
配合优化
-
场景优化(项目模板中对于图片的引用)
-
提取公共代码(manifest运行时代码)
- 提前载入webpack加载代码 inline-manifest-webpack-plugin 将webpack生成的代码插入到html中 ( inline-chunk-manifest-html-webpack-plugin)
- html-webpack-inline-chunk-plugin 选择各种各样的chunk, 将其插入到html中(*)
new HtmlWebpackPlugin({ filename: 'index.html', template: './src/htmlplugin/index.html', // inject: 'body', // 默认脚本插入在body尾部, 样式head尾部 // chunks: ['index', 'runtime'], // 不指定chunks会将上面所有打包的chunk嵌入到html中 去掉这个, 避免和上面的HtmlInlineChunkPlugin冲突 minify: { // 借助了html-minify 去压缩html collapseWhitespace: true // 压缩空格(换行符删除) } }), // 注意最好放在HtmlWebpackPlugin后面, 同时去掉该插件的chunks选项 new HtmlInlineChunkPlugin({ //希望插入到html中的chunk名称 直接将其嵌入到html的标签script中, 不是在src中, 减少网络请求 inlineChunks: ['manifest'] })
-
webpack4中如何将chunk嵌入到script内联中
optimization: { runtimeChunk: { name: 'runtime' }, } plugins: [ new InlineManifestWebpackPlugin('runtime') ]
-
-
不会产生一个web服务器, 只是监听文件的变化 webpack --watch (webpack -w)
-
CleanWebpackPlugin 打包之前清除之前打包的文件
// 每次打包都需要清除的目录 plugins: [ new CleanWebpackPlugin(['dist/watchmode']) ]
-
webpack --watch --progress --color --display-reasons --config webpack.dev.watchmode.js
-
文件变化的时候, 网页自动更新, 热加载机制; 或者启动服务的时候自动打开浏览器; 做一些远程接口的请求, 代理等
-
live-reloading 文件发生变化的时候, 自动刷新
- 不会出现打包的文件: 文件在dist是不存在的, 是在内存中 -- 只是帮助我们启动一个开发服务器, 做本地调试与开发
-
路径重定向: 网站中链接的地址配置的是线上的, 但是本地是html的地址, 就需要路径的重定向了, 将本地的html路径切换成线上的同样的路径
-
支持HTTPS
-
浏览器中显示编译的错误
-
接口代理(localhost请求远端接口, 存在跨域, 所以可以使用proxy)
-
模块热更新(live-reloading不一样, 不刷新浏览器的情况下, 更新代码, 局部更新)
-
如何配置devServer字段
-
inline设定是否使用iframe或者指定inline的方式去执行devServer
-
contentBase 提供内容路径 (内容是静态的就需要指定, 编译的内容在内存中, 直接就可以访问到), 不指定就是当前的publicPath
-
port指定端口
- historyApiFallback html5 history指定fallback规则,. 访问路径不会导致404. 让页面很方便做服务端渲染
const devServer = { port: 9001, inline: false, // 浏览器查看打包进度 // historyApiFallback: true, // 单页面使用hash#, 可以改变历史的记录, 即时一个路径, 当本地的没有文件的时候, 会报404错误 // 简单来说: http:https://localhost:9001/webpack-dev-server/tets1/2/3 不会报404 historyApiFallback: { // 引用了第三放依赖的包: connect-histroy-callback rewrites: [ { from: '/pagesA', to: '/src/devserver/pageA.html' }, { // 正则匹配, /a/b 则访问/a/b.html, 对本地请求rewrite from: /^\/([a-zA-Z0-9]+\/?)([a-zA-Z0-9]+)/, to(context) { return '/' + context.match[1] + context.match[2] + '.html' } } ] } }, }
-
https 证书
-
proxy指定远程接口代理 集成http-proxy-middleware
- target: 指定代理的地址
- changeOrigin: 改变源DOM的url (虚拟的主机上比较重要, 默认是false), 调试的时候搞成true
- headers 增加http请求的头部 (比如携带自己的cookie, UA等)
- logLevel 帮助调试 在terminal中显示代理的情况
- pathRewrite 重定向一个接口的请求, 远程是一个很复杂的地址, 可以rewrite成为一个简单的地址
proxy: { '/api': { target: 'https://cnodejs.org', // https会表示证书无效 changeOrigin: true, // 如果此项不设置, 那么请求就会报错 logLevel: 'debug' // 设置查看详细的代理信息 }, '/cgi-bin': { target: 'https://fudao.qq.com/', changeOrigin: true, headers: { origin: 'https://fudao.qq.com/pc/course.html', referer: 'https://fudao.qq.com/pc/course.html?course_id=12748', dnt: 1, 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', cookie: '登录辅导的cookie, 在cookie没有过期的情况下' } }, '/': { target: 'https://cnodejs.org', // https会表示证书无效 changeOrigin: true, // 如果此项不设置, 那么请求就会报错 pathRewrite: { '^/topics': '/api/v1/topics' } } }
-
hot打开, 支持模块热更新(在某个事件内, 将所需要替换的代码替换掉, 提供一个钩子, 在钩子触发的时候, 进行一些代码替换的操作)
- 在浏览器不刷新的情况下更新前端的代码
- 优点: (官方总结)
- 保持应用的数据状态(组件的状态, 请求数据)
- 节省调试时间
- 样式调试很快
- devServer已经集成该功能,
- hot:true
- 同时webpack.HotModuleReplacementPlugin
- 清晰查看模块的相对路径webpack.NamedModulesPlugin
- 需要写一些代码, 通过module.hot, module.hot.accept(dep, depCallback)
- 在更改样式的时候, style-loader会处理热更新所需要的操作, 在不写module也会自动更新
- js - React, Vue都有一些相关的loader, 会帮助进行相关代码的插入, 很自然的处理热更新
- 原生js: 需要写一些代码支持热更新
- 还有一些其他的API, module.hot.decline([]) 等, 有兴趣可以去官网上看下
- 需要注意的是: 如果使用MiniCssExtractPlugin, 在修改样式的时候, 不会自动刷新, 需要自己去主动刷新浏览器, 因为这里将css样式单独提取出来成为一个文件
-
openpage 指定devServer最先打开的是哪一个页面
-
lazy 让webpack在刚开始启动的时候打包任何东西, 当访问某些内容的时候才去打包编译该页面依赖, 在多页面应用中应该非常的有用, 当同时打开20多个页面的时候, webpack打包的时候非常的慢, 但是只访问一个的时候, lazy打包就会快很多了
-
overlay遮罩, 提供一个错误提示, 在打开的页面中打开一个遮罩, 在遮罩中给出编译错误的提示 (在页面中直接查看错误, 不用再命令行中查看)
-
-
开启SourceMap调试
- 很多代码都是转化过的, 用typescipt, es6, 和浏览器中的代码不是一致的, 经过编译后的调试非常困难
- sourceMap 将生成的代码和原始代码做一个映射
- 开启devTool 值有七个, 每一种的使用场景和打包速度都是不一样的
- 开发环境下有四个
- eval
- eval-source-map
- cheap-eval-source-map
- cheap-module-eval-source-map
- 开发环境下有三个- 用于开发调试, 线上bug
- source-map
- hidden-source-map
- nosource-source-map
- css调试的时候 还需要开启css-loader.option.sourcemap, less-loader.option.sourcemap等
- 如果看不到行号和详情的话, 去掉singleton:true 选项设置 (singleton:true将引用的css放在一个style标签下)
- 最重要的是开发的时候如何选择:
- 讲究重新编译的速度
- 指向代码文件名称和行数 方便调试
- 开发的时候选择: cheap-module-source-map 一点损耗性能, 但是能有提示信息, 虽然刚开始的时候编译的时候可能会慢一些, 但是在此编译的时候会比较快
- 如果是比较清晰的数据的话, 可以采用source-map
- 开发环境下有四个
- 插件的形式 webpack.SourceMapDevToolPlugin. webpack.EvalSourceMapDevToolPlugin 更加灵活
-
ESLint检查代码格式
在代码修改的时候, 检查我们的代码风格- 使编译不通过, 必须修改为标准规范webpack才会编译成功
- 安装eslint eslint-loader eslint-plugin-html(在html script标签中使用js的时候检测) eslint-friendly-formatter 报错的时候输出的格式控制 (错误和警告的输出格式)
- 配置eslint
- Javascript Standard Style
- eslint-config-standard
- eslint-plugin-promise
- eslint-plugin-standard
- eslint-plugin-import
- eslint-plugin-node
- 当然我们的IMweb eslint-config-imweb
- eslint-loader
- options.failOnWarning:true 当代码和规则有警告的话, 就不会通过编译
- options.failOnError:true
- formmater 设置第三方友好提示的formmater
- options.outputReport 输出代码检查的报告
- 在浏览器上看到代码风格检查, 可以设置devServer.overlay:true
-
开发环境和生产环境的区分
- 开发环境 (模块热更新, sourceMap, 接口代理, 代码规范检查)
- 生产环境(提取公共代码, 压缩混淆, 文件压缩或者Base64编码, TreeShaking去除无用代码)
- 共同点: 同样的入口文件, 同样的代码处理loader, 同样的解析配置(保持开发和生产的一致性)
- 区分开发环境和生产环境(流氓的写法: 两个配置文件赋值粘贴), 优雅的用法: webpack-merge 合并配置文件
- webpack.dev.conf.js
- webpack.prod.conf.js
- webpack.base.conf.js
-
让开发者更加灵活和自由定制所想要的服务 (自定义输出信息, 使用中间件帮助我们开发)
-
Express(koa) webpack-dev-middleware webpack-hot-middleware(模块热更新) http-proxy-middleware(代理) connect-history-api-fallback(地址write) opn(在命令行中打开浏览器的一个页面)
// 启动express const app = express() const port = 3000 // 获取开发环境下的配置文件 const config = require('../webpack.dev.devserver') // webpack处理 执行配置 const complier = webpack(config) // 给express使用 for (let context in proxyTable) { // 让每一个代理都通过proxyMiddleware app.use(proxyMiddleware(context, proxyTable[context])) } app.use(historyApiFallback(historyFallback)) app.use(webpackDevMiddleware(complier, { publicPath: config.output.publicPath, })) app.use(webpackHotMiddleware(complier)) app.listen(port, () => { console.log('success listen to:', port) opn('http:https://localhost:' + port) })
-
官方分析工具 (看到chunk信息, 给出优化建议)
## mac下 webpack --profile --json > stats.json ## windows webpack --profile --json | Out-file 'stats.json' -Encoding OEM ## 文件上传到 http:https://webpack.github.io/analyse/
-
官方优化方案: http:https://webpack.github.io/analyse/#hints 给出优化角度
-
Long module build chains 引用路径过长
-
Multiple references to the same module 多次引用一个模块, 修改代码
-
-
webpack-bundle-analyzer 可视化查看模块的打包机制, 分析打包和优化
// 插件的形式 // 命令行的形式 (引用之前生成的json文件) webpack-bundle-analyzer stats.json // stat.json // 告诉我们有没有错误, 有没有警告, 以及版本, hash, 打包出来chunk的名称等 // 上传到 http:https://webpack.github.io/analyse/
- 打包信息: hash, version, chunks(唯一id, 根据打包顺序分配的chunkid, 代码有可能根据顺序的不同而变化), chunkname(通过动态import, magic-commons动态指定)
- 黄色的标记: 表示文件过大
- 查看模块的信息: webpack --display-modules [0-n] 表示的是模块的id, 可以和chunk的id理解一样, webpack根据代码的顺序, 给每一个模块表上一个id, 当引用顺序发生变化的时候也会发生变化
- 影响打包速度因素
- 同时解析编译打包的文件比较多
- 依赖比较多
- 页面也比较多
- 其他: 使用loader的方式, 范围
- 办法1:
- 分开vendor和app (分离第三方代码vnedor和业务代码app) 第三方代码修改的比较少 业务代码修改比较多
- 分开是不够的, 每次仍然还会去打包vendor, 使用插件Dllplugin和DllReferencePlugin插件
- 使用Dllplugin 会生成map, 即一一对应的映射关系, 会在打包业务代码的时候引用这个映射关系, 在引用这些库的时候, webpack告诉编译器说不用打包, 这些包已经打包好了, 直接使用即可, 提高打包的速度
- 办法2:
- UglifyJsPlugin 上线之前压缩和混淆, 这是一个非常耗时的工作, 直接支持parallel, 平行处理;
- 使用cache缓存
- 办法3
- 使用HappyPack ,让文件在处理的时候, 把所有串行的工作变成并行
- 给loader使用的, 使loader并行的去处理文件
- 有一个线程池的概念 HappyPack.ThreadPool 共享文件之间的线程
- 办法4
- babel-loader babel在编译的时候也是非常消耗时间的,
- 开启options.cacheDirectory 开启缓存
- 开启include exclude, 规定打包的范围
- 其他因素
- 减少resolve (减少webpack模块的查找时间)
- devtool: 去除sourcemap, 生成sourcemap 也会消耗一定的时间, 在上线的时候考虑将其干掉
- cache-loader 将所有loader处理的结果缓存起来使用
- 最实用的方法: 升级node 升级webpack
-
默认的打包速度的6000ms多
-
引入element-ui后的打包速度12771ms
-
使用dll打包方式 (webpack.conf.dl.js)
const path = require('path'); const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const webpack = require('webpack'); module.exports = { entry: { vue: ['vue', 'vue-router'], ui: ['element-ui'], }, output: { path: path.resolve(__dirname, '../src/dll'), filename: '[name].dll.js', library: '[name]', // 告诉引用的包, 引用第三方包的形式, 如果不适用library就会产生一个全局变量 }, plugins: [ // 告诉webpack 如何去打包dll new webpack.DllPlugin({ // 不能放在dist目录下, 因为dist目录下面的是每一次build都会打包生成的, 而这里的不是每一次都需要打包生成的, 因为不会改变 path: path.resolve(__dirname, '../src/dll', '[name]-manifest.json'), // 就是上面所讲的map映射 name: '[name]', }), new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false, }, }, // parallel: true, }), ], }; // 首先对该配置进行一次打包 webpack --config build/webpack.conf.dll.js
-
生成ui.manifset.json ui.dll.js 和 vue.manifset.json vue.dll.js 这两个json非常关键, 告诉业务代码如何引用第三方类库
// webpack.prod.conf.js // 使用dll形式引用 第三方已经打包好的类库 new webpack.DllReferencePlugin({ manifest: require('../src/dll/ui-manifest.json'), }), new webpack.DllReferencePlugin({ manifest: require('../src/dll/vue-manifest.json'), }),
- 最终打包的一个消耗时间是在5199ms, 节约了50%左右的时间
-
-
在上述方式的基础上使用parallel, 同时去掉sourcemap (修改config productionSourceMap:false)
new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false, }, }, // sourceMap: config.build.productionSourceMap, sourceMap: false, parallel: true, cache: true, }), // 最终的一个打包时间为3002ms
-
减少babel-loader的范围
{ test: /\.js$/, loader: 'babel-loader', // client是在开发环境中使用的, 减少include的范围 include: [resolve('src')], // include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')], }, // 最终的一个打包速度 2781ms
-
happypack (npm install happy-pack --save-dev)
- happy是一个loader, 让我们的loader并行处理, 使我们的处理非常快, 当文件非常小的时候比明显, 未必如同想象中的一样
- (因为业务代码量比较少)未必会带来很好的效益 (4063ms)
// webpack.prod.conf.js plugins中 // 实例化HappyPack new HappyPackPlugin({ // 此id供 base.conf中的happypack使用 id: 'vue', // 进程实际处理的loader loaders: [{ loader: 'vue-loader', option: vueLoaderConfig, }], }), // webpack.base.conf.js module.rules中 { test: /\.js$/, loader: 'babel-loader', // client是在开发环境中使用的, 减少include的范围 include: [resolve('src')], // include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')], },
-
原则
- 让webpack并行处理可以并行处理的任务 uglifyJsPlugin和 HappyPack
- 尽可能减少webpack的打包任务, 通过分离第三方和我们的业务, 使用babel限定打包范围, 同时尽可能的利用缓存
- 什么是长缓存优化
- webpack-dev-server, Express+WebpackMiddleWare使用Express搭建, HMR(模块热加载热更新机制), plugins, 长缓存的用法和实现原理
- 零配置
- 零配置就意味着webpack4具有默认配置,webpack运行时,会根据
mode
的值采取不同的默认配置。如果你没有给webpack传入mode,会抛出错误,并提示我们如果要使用webpack就需要设置一个mode。 - mode development none production
- webpack4把很多插件相关的配置都迁移到了optimization中,但是我们看看官方文档对optimization的介绍简直寥寥无几,而在默认配置的代码中,webpack对optimization的配置有十几项,反正我是怕了
- 这里还有一些其他的配置没有贴出来, 可以去
- 零配置就意味着webpack4具有默认配置,webpack运行时,会根据
- loaders和plugins升级
- 先说说
extract-text-webpack-plugin
,这个插件主要用于将多个css合并成一个css,减少http请求,命名时支持contenthash(根据文本内容生成hash)。但是webpack4使用有些问题,所以官方推荐使用mini-css-extract-plugin
。
- 先说说
- webpack工程化总结
- 实时编译的服务
- 好的开发服务
- 自动优化
- 思想
- 一切皆为模块
- 急速的调试响应速度(HMR)
- 优化应该自动完成(当我们设置一些配置后)
- webpack4
- 零配置(主流配置)
- 更快更小