假期没有出去玩,但是我觉得很充实吼/(ㄒoㄒ)/~~
根据Sean在 frontend masters上的课程加上个人的理解 Sean 是 webpack 的核心贡献者哦?课程代码
为什么要使用webpack
传统方法在浏览器中执行JS
- 一个功能加载一个script 标签 e.g.
Jquery
,Swiper
- 加载一个巨大的JS文件。
script标签加载的弊端
- 扩展性太差,标签很多的情况下维持标签的顺序很痛苦。
- 全局变量污染
- 加载过多的JS文件有加载性能问题,因为浏览器的并行连接数有限制。可以参考html - Max parallel http connections in a browser? - Stack Overflow
单独一个JS文件的弊端
这里应该是说所有代码写在一起,不是按照模块组织最后打包出一个文件来的。
- 不同功能的作用域问题
- 用户需要加载的文件过大。
- 维护性和可读性很差。
立即执行函数IIFEs
Immediately invoked function expressions
var outerScope = 1;
const whatever = (function(dataNowUsedInside) {
var outerScope = 4;
return {
someAttribute: "you want"
};
})(1);
console.log(outerScope); //1
复制代码
立即执行函数可以解决作用域冲突的问题,如上因为函数作用域的存在不会污染外部作用域。 PS: 另外上面的这个暴露出一个模块的模式叫Revealing Module pattern 可以参考 Learning JavaScript Design Patterns
Make
, Gulp
, Grunt
, Broccoli
, Brunch
这些工具通过一个文件当做一个立即执行函数,将这些立即执行函数连接起来打包成一个文件。
这些工具的弊端:
- 每次更改一个文件都要重新构建所有文件
- 无法剔除没有使用过的代码,比如引入
Lodash
, 就用了几个函数,结果500Kb的文件都被引入了。 - 立即执行函数过多的话可能会造成性能问题。 参考这篇文章The cost of small modules | Read the Tea Leaves。文章里提到一个是函数内嵌套函数,还有在关联数组中查看模块在模块越来越多的情况下会暴露出意外的性能问题。另外立即执行函数会导致引擎立刻解析函数 (eager parse),大量的立即执行函数也会导致应用解析变慢。
- 没法做懒加载。
Javascript模块优缺点
CommonJS
node
里没有标签,怎么加载JS代码呢,于是出来了CommonJS.
- 但是 CommonJS 不支持浏览器
- 没有动态绑定,什么是动态绑定可以参考 深入es module
- CommonJS解析算法慢,因为它是同步的
- 没有静态分析,没法剔除无用代码
打包器
开发者不愿意从某个库的网上下载JS文件然后放到项目中来,他们想通过NPM分享模块代码。但是NPM上的是CommonJs模块的,所以就出现了Browserify
之类的工具把CommonJS风格的代码解析require
声明,按照你引用的顺序打包成浏览器可以执行的代码。
AMD
除了CommonJS还有很多模块加载模式。甚至还有AMD+CommonJS的模块加载模式。
没有真正的模块系统,没有node 浏览器都支持的。直到ESM
的出现。
ESM
ES2015和模块是独立的两个部分。模块标准原名是harmony modules specification。你甚至可以用ES3的语法搭配模块使用。 ESM的问题:
- node 中的ESM尚未实现。
- 兼容问题。
webpack 能做什么
webpack 是一个模块打包器
- 库的作者使用他们喜欢的模块系统,AMD, CommonJS, webpack 可以让你使用任何的模块格式甚至混合,将它们最终打包成浏览器可以执行的代码。
- 在构建阶段创建动态打包模块(懒加载的模块)
- 不仅打包JS资源还包括html, css甚至图片
调试webpack
- 按照调试node的方法
node --inspect --inspect-brk ./node_modules/webpack/bin/webpack.js
复制代码
之后在浏览器中打开,可以参考debugger 2. 利用vscode
"configurations": [
{
"type": "node",
"request": "launch",
"name": "webpack",
"program": "${workspaceFolder}/node_modules/webpack/bin/webpack.js"
}
]
复制代码
可以参考下debugging-webpack
webpack 基础概念
什么是依赖图(dependency graph)
webpack根据模块之间的依赖关系递归构建而成 dependency-graph
entry
告诉webpack从那个文件开始构建它的依赖图。webpack4默认配置是src/index.js
output
告诉webpack打包好的文件的位置名称等。webpack4默认配置会打包到dist/main.js
loader
webpack开箱只理解js和json文件。loader可以将其他模块加载进来转换成js 模块,然后加载到依赖图里。webpack默认使用的是acorn 解析js文件,acorn默认只会处理stage4的提案。所以比较新的特性webpack是处理不了的,这个时候可以借助babel 这里举两个loader
babel-loader
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
复制代码
babel-loader8.x需要配合使用的是@babel-core以及@babel/preset-env
npm install -D babel-loader @babel/core @babel/preset-env
复制代码
babel-loader: webpack和babel之间的桥梁 @babel/core: 解析文件并且生成文件 @babel/preset-env: 相应的转换规则
css loader
npm install -D css-loader style-loader
复制代码
{
test: /\.css$/,
loaders: ['style-loader', 'css-loader']
}
复制代码
loader解析顺序是从右向左可以 可以理解成这样styleLoader(cssLoader())
一个方法的输出是另一个方法的参数。 css-loader: 负责将css文件转换成js style-loader: 将css内容插入标签中 ps:我在安装了相关loader之后, 在vscode里去尝试从node_modules目录里找发现怎么也找不到相关loader o(╯□╰)o,最后我直接打开了文件目录找到了。。。
如何写一个loader
write-a-loader 总结下就是loader是一个函数,在webpack配置里可以配置loader的别名,以及你自己写的loader的解析位置。 函数的参数就是上一个loader处理好后的字符串。loader也分同步和异步的,babel-loader就是异步的。
plugin
plugin可以在webpack的构建的任意时刻执行一些特定任务,比如打包优化,插入环境变量等。 webpack源码有很大部分都是插件写的,具体会再出一篇详细讲下webpack是如何工作的,这里就简单介绍下。
如何写一个插件
class myFirstWebpackPlugin {
apply(compiler){
compiler.hooks.done.tapAsync('myFirstWebpackPlugin',(stats, cb)=>{
cb()
})
}
}
module.exports = myFirstWebpackPlugin
复制代码
以上代码是基于webpack4的,3.x的版本是如下
class myFirstWebpackPlugin {
apply(compiler){
compiler.plugin("done",(stats)=>{
})
}
}
module.exports = myFirstWebpackPlugin
复制代码
3.x版本的hook是字符串形式,需要人为去记忆,在webpack4中将其作为变量的属性,文档化后在编辑器中输入会有提示,目前尚未文档化完成。
插件可以在webpack构建的任意时刻执行,和钩子函数一样,提前在这些时间点注册回调函数,插件的钩子同样可以是同步或者异步的。上面的实例是在构建完成后执行,回调函数的参数里包括一系列统计数据,构建开始时间结束时间等。
webpack代码拆分(code splitting)
代码拆分的用途:
- 不需要在首屏加载的比较大的第三方库比如three.js
- 模态框提示框等用户可能不会去点击的
- 路由组件
代码拆分的好处:
- 提升首屏加载速度,提升用户体验特别是移动端
- 有助于提升SEO,google会降低加载慢的网站的权重
webpack会将import()
引用的模块打包到单独打包到输出目录下,webpack内置的acorn
解析器默认是只解析stage4的,import()
仍然在stage3,proposals。这是因为webpack引用了acorn-dynamic-import
可以支持在不使用babel的情况下解析import()
语法。如果项目中使用了babel-loader
需要再配置@babel/plugin-syntax-dynamic-import
,否则babel会报错,因为babel也是需要将代码解析转换成抽象语法树的。 webpack代码拆分分为静态和动态代码拆分,但是无论哪种,都是将异步请求的模块提前打包到输出目录中。
静态代码拆分(static code splitting)
// main.js
const getFooter = () => import('./footer');
button.addEventListener('click',e=>{
getFooter().then(m=>{
document.body.appendChild(m.footer)
})
})
// footer.js
const footer = document.createElement("footer");
export { footer };
复制代码
import()
返回的是一个promise
动态代码拆分(dynamic code splitting)
const setButtonStyle = color => import(`./button-style/${color}`);
复制代码
上面的./button-style
是部分地址,webpack会将这个地址下面的所有js模块进行单独打包。这个可以用在当这个目录下有很多文件需要代码拆分的时候。
魔法注释(magic comment)
内联注释控制webpack动态打包行为,为什么要使用它?因为这样不会增加新语法,不和动态import以及loader规范冲突。
import(
/* webpackMode: "lazy-once" */
`./button-style/${color}`);
复制代码
比如上面这个webpackMode
默认是lazy
它会将./button-style
下的所有模块都单独打包;lazy-once
则会将其下面的所有模块打成一个包常用于开发环境减少打包次数。 更多注释选项请参考官网magic-comments
流行库中的代码拆分
vue-code-splitting-pattern
react-loadable