Webpack从入门到进阶(三)—附沿路学习案例代码
Webpack从入门到进阶(一)—附沿路学习案例代码
Webpack从入门到进阶(二)—附沿路学习案例代码
一、Webpack简介
1、前端发展的几个阶段
无论是作为专业的开发者还是接触互联网的普通人,其实都能深刻的感知到Web前端的发展是非常快速的
-
对于开发者来说我们会更加深有体会;
-
从后端渲染的JSP、PHP,到前端原生JavaScript,再到jQuery开发,再到目前的三大框架Vue、React、Angular;
-
开发方式也从原来的JavaScript的ES5语法,到ES6、7、8、9、10,到TypeScript,包括编写CSS的预处理器less、scss等;
前端开发的复杂化
前端开发目前我们面临哪些复杂的问题呢?
- 比如开发过程中我们需要通过模块化的方式来开发;
- 比如也会使用一些高级的特性来加快我们的开发效率或者安全性,比如通过ES6+、TypeScript开发脚本逻辑,通过sass、less等方式来编写css样式代码;
- 比如开发过程中,我们还希望试试的监听文件的变化来并且反映到浏览器上,提高开发的效率;
- 比如开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化;
- 等等
2、前端三个框架的脚手架
目前前端流行的三大框架:Vue、React、Angular
- 但是事实上,这三大框架的创建过程我们都是借助于脚手架(CLI)的;
- 事实上Vue-CLI、create-react-app、Angular-CLI都是基于webpack来帮助我们支持模块化、less、TypeScript、打包优化等的;
3、Webpack是什么?
webpack是一个静态的模块化打包工具,为现代的JavaScript应用程序;
webpack is a static module bundler for modern JavaScript applications.
我们来对上面的解释进行拆解:
-
打包bundler:webpack可以将帮助我们进行打包,所以它是一个打包工具
-
静态的static:这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器);
-
模块化module:webpack默认支持各种模块化开发,ES Module、CommonJS、AMD等;
-
现代的modern:我们前端说过,正是因为现代前端开发面临各种各样的问题,才催生了webpack的出现和发展;
Webpack 是一个前端资源加载和打包工具。所谓的模块就是在平时的前端开发中,用到一些静态资源,如JavaScript、CSS、图片等文件,webpack就将这些静态资源文件称之为模块。 webpack支持AMD和CommonJS,以及其他的一些模块系统,并且兼容多种JS书写规范,可以处理模块间的依赖关系,所以具有更强大的JS模块化的功能,它能对静态资源进行统一的管理以及打包发布。
作为一款 Grunt和Gulp的替代产品,Webpack受到大多数开发者的喜爱,因为它能够编译打包CSS,做CSS预处理,对JS的方言进行编译,打包图片,代码压缩等等。
与其他的构建工具相比,Webpack具有如下的一些优势:
- 对 CommonJS 、 AMD 、ES6 的语法做了兼容;
- 对 js、css、图片等资源文件都支持打包;
- 串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对 CoffeeScript、ES6的支持;
- 有独立的配置文件 webpack.config.js;
- 可以将代码切割成不同的 chunk,实现按需加载,降低了初始化时间;
- 支持 SourceUrls 和 SourceMaps,易于调试;
- 具有强大的 Plugin 接口,大多是内部插件,使用起来比较灵活;
- webpack 使用异步 IO 并具有多级缓存。这使得 webpack 很快且在增量编译上更加快。
在Webpack的世界里有两个最核心的概念:
-
一切皆模块
正如js文件可以是一个“模块(module)”一样,其他的(如css、image或html)文件也可视作模 块。因此,你可以require(‘myJSfile.js’)亦可以require(‘myCSSfile.css’)。这意味着我们可以将事物(业务)分割成更小的易于管理的片段,从而达到重复利用等的目的。 -
按需加载
传统的模块打包工具(module bundlers)最终将所有的模块编译生成一个庞大的bundle.js文件。但是在真实的app里边,“bundle.js”文件可能有10M到15M之大可能会导致应用一直处于加载中状态。因此Webpack使用许多特性来分割代码然后生成多个“bundle”文件,而且异步加载部分代码以实现按需加载。
Webpack官网图片
工作中的webpack
4、webpack和vite
我们来提一个问题:webpack会被vite取代吗?
- vite推出后确实引起了很多的反响,也有很多人看好vite的发展(包括我);
但是目前vite取代webpack还有很长的路要走
- 目前vue的项目支持使用vite,也支持使用webpack;
- React、Angular的脚手架目前没有支持,暂时也没有转向vite的打算;
- vite最终打包的过程,依然需要借助于rollup来完成;
vite的核心思想并不是首创
- 事实上,vite的很多思想和之前的snowpack是重合的,而且相对目前来说snowpack会更加成熟;
- 当然,后续发展来看vite可能会超越snowpack;
webpack的更新迭代
- webpack在发展的过程中,也会不断改进自己,借鉴其他工具的一些优势和思想;
- 在这么多年的发展中,无论是自身的优势还是生态都是非常强大的;
关于vite的思考
我的个人观点:
学习任何的东西,重要的是学习核心思想:
- 我们不能学会了JavaScript,有一天学习TypeScript、Java或者其他语言,所有的内容都是从零开始的;
- 我们在掌握了react之后,再去学习vue、Angular框架,也应该是可以快速上手的;
任何工具的出现,都是更好的服务于我们开发:
-
无论是vite的出现,还是以后新的工具的出现,不要有任何排斥的思想;
-
我们要深刻的明白,工具都是为了更好的给我们提供服务;
-
不可能出现了某个工具,让我们的开发效率变得更低,而这个工具却可以变得非常流行,这是不存在的;
5、vue-cli-service运行过程
6、Webpack的官方文档
webpack的官方文档是https://webpack.js.org/
- webpack的中文官方文档是https://webpack.docschina.org/
- DOCUMENTATION:文档详情,也是我们最关注的
点击DOCUMENTATION来到文档页:
- API:API,提供相关的接口,可以自定义编译的过程(比如自定义loader和Plugin可以参考该位置的API)
- BLOG:博客,等同于上一个tab的BLOG,里面有一些博客文章;
- CONCEPTS:概念,主要是介绍一些webpack的核心概念,比如入口、出口、Loaders、Plugins等等,但是这里并没有一些对它们解析的详细API;
- CONFIGURATION:配置,webpack详细的配置选项,都可以在这里查询到,更多的时候是作为查询手册;
- GUIDES:指南,更像是webpack提供给我们的教程,我们可以按照这个教程一步步去学习webpack的使用过程;
- LOADERS:loaders,webpack的核心之一,常见的loader都可以在这里查询到用法,比如css-loader、babel-loader、less-loader等等;
- PLUGINS:plugins,webpack的核心之一,常见的plugin都可以在这里查询到用法,比如BannerPlugin、CleanWebpackPlugin、MiniCssExtractPlugin等等;
- MIGRATE:迁移,可以通过这里的教程将webpack4迁移到webpack5等;
7、Webpack的依赖和安装
Webpack的运行是依赖Node环境的,所以我们电脑上必须有Node环境
- 所以我们需要先安装Node.js,并且同时会安装npm;
- Node官方网站:https://nodejs.org/
Webpack的安装
webpack的安装目前分为两个:webpack、webpack-cli
那么它们是什么关系呢?
- 执行webpack命令,会执行node_modules下的.bin目录下的webpack;
- webpack在执行时是依赖webpack-cli的,如果没有安装就会报错;
- 而webpack-cli中代码执行时,才是真正利用webpack进行编译和打包的过程;
- 所以在安装webpack时,我们需要同时安装webpack-cli(第三方的脚手架事实上是没有使用webpack-cli的,而是类似于自己的vue-service-cli的东西)
npm install webpack webpack-cli –g # 全局安装
npm install webpack webpack-cli –D # 局部安装
传统开发存在的问题
我们的代码存在什么问题呢?某些语法浏览器是不认识的(尤其在低版本浏览器上)
- 1.使用了ES6的语法,比如const、箭头函数等语法;
- 2.使用了ES6中的模块化语法;
- 3.使用CommonJS的模块化语法;
- 4.在通过script标签引入时,必须添加上 type=“module” 属性;
显然,上面存在的问题,让我们在发布静态资源时,是不能直接发布的,因为运行在用户浏览器必然会存在各种各样的兼容性问题。
- 我们需要通过某个工具对其进行打包,让其转换成浏览器可以直接识别的语法;
8、webpack默认打包
我们可以通过webpack进行打包,之后运行打包之后的代码
- 在目录下直接执行 webpack 命令
webpack
生成一个dist文件夹,里面存放一个main.js的文件,就是我们打包之后的文件:
- 这个文件中的代码被压缩和丑化了;
- 我们暂时不关心他是如何做到的,后续我讲webpack实现模块化原理时会再次讲到;
- 另外我们发现代码中依然存在ES6的语法,比如箭头函数、const等,这是因为默认情况下webpack并不清楚我们打包后的文件是否需要转成ES5之前的语法,后续我们需要通过babel来进行转换和设置;
我们发现是可以正常进行打包的,但是有一个问题,webpack是如何确定我们的入口的呢?
- 事实上,当我们运行webpack时,webpack会查找当前目录下的 src/index.js作为入口;
- 所以,如果当前项目中没有存在src/index.js文件,那么会报错;
9、Webpack配置文件
在通常情况下,webpack需要打包的项目是非常复杂的,并且我们需要一系列的配置来满足要求,默认配置必然是不可以的
我们可以在根目录下创建一个webpack.config.js文件,来作为webpack的配置文件:
const path = require('path');
module.exports = {
entry: "./src/main.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "./dist")
}
}
继续执行webpack命令,依然可以正常打包
指定配置文件
但是如果我们的配置文件并不是webpack.config.js的名字,而是其他的名字呢?
-
比如我们将webpack.config.js修改成了 wk.config.js;
-
这个时候我们可以通过 --config 来指定对应的配置文件;
webpack --config wk.config.js
但是每次这样执行命令来对源码进行编译,会非常繁琐,所以我们可以在package.json中增加一个新的脚本:
{
"scripts": {
"build": webpack --config wk.config.js
}
}
之后我们执行 npm run build来打包即可。
10、Webpack依赖图
webpack到底是如何对我们的项目进行打包的呢?
-
事实上webpack在处理应用程序时,它会根据命令或者配置文件找到入口文件;
-
从入口开始,会生成一个 依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块(比如.js文件、css文件、图片、字体等);
-
然后遍历图结构,打包一个个模块(根据文件的不同使用不同的loader来解析);
二、Webpack知识点
七、DLL
1、DLL认识、打包和使用
认识DLL库
DLL是什么呢?
-
DLL全程是动态链接库(Dynamic Link Library),是为软件在Windows中实现共享函数库的一种实现方式;
-
那么webpack中也有内置DLL的功能,它指的是我们可以将可以共享,并且不经常改变的代码,抽取成一个共
享的库;
- 这个库在之后编译的过程中,会被引入到其他项目的代码中;
DDL库的使用分为两步:
-
第一步:打包一个DLL库;
-
第二步:项目中引入DLL库
注意:在升级到webpack4之后,React和Vue脚手架都移除了DLL库(下面的vue作者的回复)
打包一个DLL库
如何打包一个DLLPlugin?(创建一个新的项目)
- webpack帮助我们内置了一个DllPlugin可以帮助我们打包一个DLL的库文件;
module.exports = {
entry: {
react: ["react", "react-dom"]
},
output: {
path: path.resolve(__dirname, "./dll"),
filename: "dll_[name].js",
library: 'dll_[name]'
},
optimization: {
minimizer: [
new TerserPlugin({
extractComments: false
})
]
},
plugins: [
new webpack.DllPlugin({
name: "dll_[name]",
path: path.resolve(__dirname, "./dll/[name].manifest.json")
})
]
}
使用打包的DLL库
如果我们在我们的代码中使用了react、react-dom,我们有配置splitChunks的情况下,他们会进行分包,打包到一个独立的chunk中。
-
但是现在我们有了dll_react,不再需要单独去打包它们,可以直接去引用dll_react即可:
-
第一步:通过DllReferencePlugin插件告知要使用的DLL库;
-
第二步:通过AddAssetHtmlPlugin插件,将我们打包的DLL库引入到Html模块中;
new webpack.DllReferencePlugin({
context: resolveApp("./"),
manifest: resolveApp("./dll/react.manifest.json")
}),
new AddAssetHtmlPlugin({
filepath: resolveApp('./dll/dll_react.js')
})
2、Terser
Terser介绍和安装
什么是Terser呢?
-
Terser是一个JavaScript的解释(Parser)、Mangler(绞肉机)/Compressor(压缩机)的工具集;
-
早期我们会使用 uglify-js来压缩、丑化我们的JavaScript代码,但是目前已经不再维护,并且不支持ES6+的语法;
-
Terser是从 uglify-es fork 过来的,并且保留它原来的大部分API以及适配 uglify-es和uglify-js@3等;
也就是说,Terser可以帮助我们压缩、丑化我们的代码,让我们的bundle变得更小。
因为Terser是一个独立的工具,所以它可以单独安装:
# 全局安装
npm install terser -g
# 局部安装
npm install terser
命令行使用Terser
我们可以在命令行中使用Terser:
terser [input files] [options]
# 举例说明
terser js/file1.js -o foo.min.js -c -m
我们这里来讲解几个Compress option和Mangle option:
-
因为他们的配置非常多,我们不可能一个个解析,更多的查看文档即可;
-
https://github.com/terser/terser#compress-options
-
https://github.com/terser/terser#mangle-options
Compress和Mangle的options
Compress option:
-
arrows:class或者object中的函数,转换成箭头函数;
-
arguments:将函数中使用 arguments[index]转成对应的形参名称;
-
dead_code:移除不可达的代码(tree shaking);
-
其他属性可以查看文档;
Mangle option
-
toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换);
-
keep_classnames:默认值是false,是否保持依赖的类名称;
-
keep_fnames:默认值是false,是否保持原来的函数名称;
-
其他属性可以查看文档;
npx terser ./src/abc.js -o abc.min.js -c
arrows,arguments=true,dead_code -m
toplevel=true,keep_classnames=true,keep_fnames=true
Terser在webpack中配置
真实开发中,我们不需要手动的通过terser来处理我们的代码,我们可以直接通过webpack来处理:
-
在webpack中有一个minimizer属性,在production模式下,默认就是使用TerserPlugin来处理我们的代码的;
-
如果我们对默认的配置不满意,也可以自己来创建TerserPlugin的实例,并且覆盖相关的配置;
首先,我们需要打开minimize,让其对我们的代码进行压缩(默认production模式下已经打开了)
其次,我们可以在minimizer创建一个TerserPlugin:
-
extractComments:默认值为true,表示会将注释抽取到一个单独的文件中;
在开发中,我们不希望保留这个注释时,可以设置为false;
-
parallel:使用多进程并发运行提高构建的速度,默认值是true,并发运行的默认数量: os.cpus().length - 1;
我们也可以设置自己的个数,但是使用默认值即可;
-
terserOptions:设置我们的terser相关的配置
compress:设置压缩相关的选项;
mangle:设置丑化相关的选项,可以直接设置为true;
toplevel:底层变量是否进行转换;
keep_classnames:保留类的名称;
keep_fnames:保留函数的名称;
CSS的压缩
另一个代码的压缩是CSS:
-
CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等;
-
CSS的压缩我们可以使用另外一个插件:css-minimizer-webpack-plugin;
-
css-minimizer-webpack-plugin是使用cssnano工具来优化、压缩CSS(也可以单独使用);
第一步,安装 css-minimizer-webpack-plugin:
npm install css-minimizer-webpack-plugin -D
第二步,在optimization.minimizer中配置
minimizer: [
new TerserPlugin({....}),
new CssMinimizerPlugin({
parallel: true
})
]
3、Tree Shaking
什么是Tree Shaking呢?
-
Tree Shaking是一个术语,在计算机中表示消除死代码(dead_code);
-
最早的想法起源于LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式编程时,尽量使用纯函数的原因之一);
-
后来Tree Shaking也被应用于其他的语言,比如JavaScript、Dart;
JavaScript的Tree Shaking:
-
对JavaScript进行Tree Shaking是源自打包工具rollup(后面我们也会讲的构建工具);
-
这是因为Tree Shaking依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系);
-
webpack2正式内置支持了ES2015模块,和检测未使用模块的能力;
-
在webpack4正式扩展了这个能力,并且通过 package.json的 sideEffects属性作为标记,告知webpack在编译时,哪里文件可以安全的删除掉;
-
webpack5中,也提供了对部分CommonJS的tree shaking的支持;
https://github.com/webpack/changelog-v5#commonjs-tree-shaking
webpack实现Tree Shaking
事实上webpack实现Tree Shaking采用了两种不同的方案:
-
usedExports:通过标记某些函数是否被使用,之后通过Terser来进行优化的;
-
sideEffects:跳过整个模块/文件,直接查看该文件是否有副作用;
usedExports
将mode设置为development模式:
-
为了可以看到 usedExports带来的效果,我们需要设置为 development 模式
-
因为在 production 模式下,webpack默认的一些优化会带来很大额影响。
设置usedExports为true和false对比打包后的代码:
-
在usedExports设置为true时,会有一段注释:unused harmony export mul;
-
这段注释的意义是什么呢?告知Terser在优化时,可以删除掉这段代码;
这个时候,我们讲 minimize设置true:
-
usedExports设置为false时,mul函数没有被移除掉;
-
usedExports设置为true时,mul函数有被移除掉;
所以,usedExports实现Tree Shaking是结合Terser来完成的。
sideEffects
sideEffects用于告知webpack compiler哪些模块时有副作用的:
-
副作用的意思是这里面的代码有执行一些特殊的任务,不能仅仅通过export来判断这段代码的意义;
-
副作用的问题,在讲React的纯函数时是有讲过的;
在package.json中设置sideEffects的值:
-
如果我们将sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports;
-
如果有一些我们希望保留,可以设置为数组;
比如我们有一个format.js、style.css文件:
-
该文件在导入时没有使用任何的变量来接受;
-
那么打包后的文件,不会保留format.js、style.css相关的任何代码;
"sideEffects": [
"./src/util/format.js",
"*.css"
]
Webpack中tree shaking的设置
所以,如何在项目中对JavaScript的代码进行TreeShaking呢(生成环境)?
-
在optimization中配置usedExports为true,来帮助Terser进行优化;
-
在package.json中配置sideEffects,直接对模块进行优化;
CSS实现Tree Shaking
上面我们学习的都是关于JavaScript的Tree Shaking,那么CSS是否也可以进行Tree Shaking操作呢?
-
CSS的Tree Shaking需要借助于一些其他的插件;
-
在早期的时候,我们会使用PurifyCss插件来完成CSS的tree shaking,但是目前该库已经不再维护了(最新更新也是在4年前了);
-
目前我们可以使用另外一个库来完成CSS的Tree Shaking:PurgeCSS,也是一个帮助我们删除未使用的CSS的工具;
安装PurgeCss的webpack插件:
npm install purgecss-webpack-plugin -D
配置PurgeCss
配置这个插件(生成环境):
-
paths:表示要检测哪些目录下的内容需要被分析,这里我们可以使用glob;
-
默认情况下,Purgecss会将我们的html标签的样式移除掉,如果我们希望保留,可以添加一个safelist的属性;
new PurgeCssPlugin({
paths: glob.sync(`${resolveApp("./src")}/**/*`, {nodir: true}),
safelist: function() {
return {
standard: ["body", "html"]
}
}
})
purgecss也可以对less文件进行处理(所以它是对打包后的css进行tree shaking操作);
Scope Hoisting
什么是Scope Hoisting呢?
-
Scope Hoisting从webpack3开始增加的一个新功能;
-
功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快;
默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:
-
无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;
-
Scope Hoisting可以将函数合并到一个模块中来运行;
使用Scope Hoisting非常的简单,webpack已经内置了对应的模块:
-
在production模式下,默认这个模块就会启用;
-
在development模式下,我们需要自己来打开该模块;
new webpack.optimize.ModuleConcatenationPlugin()
4、HTTP压缩
HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式;
HTTP压缩的流程什么呢?
-
第一步:HTTP数据在服务器发送前就已经被压缩了;(可以在webpack中完成)
-
第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式;
第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器;
目前的压缩格式
目前的压缩格式非常的多:
-
compress – UNIX的“compress”程序的方法(历史性原因,不推荐大多数应用使用,应该使用gzip或deflate);
-
deflate – 基于deflate算法(定义于RFC 1951)的压缩,使用zlib数据格式封装;
-
gzip – GNU zip格式(定义于RFC 1952),是目前使用比较广泛的压缩算法;
-
br – 一种新的开源压缩算法,专为HTTP内容的编码而设计;
Webpack对文件压缩
webpack中相当于是实现了HTTP压缩的第一步操作,我们可以使用CompressionPlugin。
第一步,安装CompressionPlugin:
npm install compression-webpack-plugin -D
第二步,使用CompressionPlugin即可:
new CompressionPlugin({
test: /\.(css|js)$/i,
threshold: 0,
minRatio: 0.8,
algorithm: "gzip",
// exclude
// include
}),
HTML文件中代码的压缩
我们之前使用了HtmlWebpackPlugin插件来生成HTML的模板,事实上它还有一些其他的配置:
inject:设置打包的资源插入的位置
- true、 false 、body、head
cache:设置为true,只有当文件改变时,才会生成新的文件(默认值也是true)
minify:默认会使用一个插件html-minifier-terser
InlineChunkHtmlPlugin
另外有一个插件,可以辅助将一些chunk出来的模块,内联到html中:
-
比如runtime的代码,代码量不大,但是是必须加载的;
-
那么我们可以直接内联到html中;
这个插件是在react-dev-utils中实现的,所以我们可以安装一下它:
npm install react-dev-utils -D
在production的plugins中进行配置:
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.*\.js/,])
封装Library
webpack可以帮助我们打包自己的库文件,比如我们需要打包一个coderwhy_utils的一个库。
// lib/format.js
export function dateFormat() {
return "2021-11-11";
}
// lib/math.js
export function sum(num1, num2) {
return num1 + num2;
}
export function mul(num1, num2) {
return num1 + num2;
}
// index.js
import * as math from './lib/math';
import * as format from './lib/format';
console.log("abc");
export {
math,
format
}
打包Library
配置webpack.config.js文件
const path = require('path');
module.exports = {
mode: "production",
entry: "./index.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "coderwhy_utils.js",
// AMD/CommonJS/浏览器
// CommnJoS: 社区规范的CommonJS, 这个里面是没有module对象
// CommonJS2: Node实现的CommonJS, 这个里面是有module对象, module.exports
libraryTarget: "umd",
library: "coderwhyUtils",
globalObject: "self"
}
}
!(function (e, t) {
"object" == typeof exports && "object" == typeof module
? (module.exports = t())
: "function" == typeof define && define.amd
? define([], t)
: "object" == typeof exports
? (exports.coderwhyUtils = t())
: (e.coderwhyUtils = t());
})(this, function () {})
八、打包分析和webpack源码
1、打包分析
分析一:打包的时间分析
如果我们希望看到每一个loader、每一个Plugin消耗的打包时间,可以借助于一个插件:speed-measure-webpack-plugin
-
注意:该插件在最新的webpack版本中存在一些兼容性的问题(和部分Plugin不兼容)
-
截止2021-3-10日,但是目前该插件还在维护,所以可以等待后续是否更新;
-
我这里暂时的做法是把不兼容的插件先删除掉,也就是不兼容的插件不显示它的打包时间就可以了;
第一步,安装speed-measure-webpack-plugin插件
npm install speed-measure-webpack-plugin -D
第二步,使用speed-measure-webpack-plugin插件
-
创建插件导出的对象 SpeedMeasurePlugin;
-
使用 smp.wrap 包裹我们导出的webpack配置;
// 测量打包时间的插件
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [new Myplugin(), new MyOtherPlugin()]
})
分析二:打包后文件分析
方案一:生成一个stats.json的文件
"buiebpack ld:stats": "w--config ./config/webpack.common.js --env production --profile --json=stats.json",
通过执行npm run build:status可以获取到一个stats.json的文件:
-
这个文件我们自己分析不容易看到其中的信息;
-
可以放到 http://webpack.github.com/analyse,进行分析
方案二:使用webpack-bundle-analyzer工具
- 另一个非常直观查看包大小的工具是webpack-bundle-analyzer。
第一步,我们可以直接安装这个工具:
npm install webpack-bundle-analyzer -D
第二步,我们可以在webpack配置中使用该插件:
// 打包大小分析的插件
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
在打包webpack的时候,这个工具是帮助我们打开一个8888端口上的服务,我们可以直接的看到每个包的大小。
-
比如有一个包时通过一个Vue组件打包的,但是非常的大,那么我们可以考虑是否可以拆分出多个组件,并且对其进行懒加载;
-
比如一个图片或者字体文件特别大,是否可以对其进行压缩或者其他的优化处理;
2、webpack源码
Webpack的启动流程
Webpack源码阅读
第一步:下载webpack的源码
- https://github.com/webpack/webpack
第二步:安装项目相关的依赖
- npm install
第三步:编写自己的源代码
- 这里我创建了一个 why 文件夹,里面存放了一些代码
第四步:编写webpack的配置文件
- webpack.config.js
第五步:编写启动的文件build.js
创建Compiler
Compiler中run方法执行的Hook
Compilation对Module的处理
module的build阶段
输出asset阶段
Compiler和Compilation的区别
Compiler和Compilation的区别
在webpack构建的之初就会创建的一个对象, 并且在webpack的整个生命周期都会存在(before - run - beforeCompiler - compile -make - finishMake - afterCompiler - done)
只要是做webpack的编译, 都会先创建一个Compiler
Compilation是到准备编译模块(比如main.js), 才会创建Compilation对象
主要是存在于 compile - make 阶段主要使用的对象
watch -> 源代码发生改变就需要重新编译模块
Compiler可以继续使用(如果我修改webpack的配置, 那么需要重新执行run run build)
Compilation需要创建一个新的Compilation对象
九、自定义Loader
1、创建自己的Loader
Loader是用于对模块的源代码进行转换(处理),之前我们已经使用过很多Loader,比如css-loader、style-loader、babel-loader等。
这里我们来学习如何自定义自己的Loader:
-
Loader本质上是一个导出为函数的JavaScript模块;
-
loader runner库会调用这个函数,然后将上一个loader产生的结果或者资源文件传入进去;
编写一个hy-loader01.js模块这个函数会接收三个参数:
-
content:资源文件的内容;
-
map:sourcemap相关的数据;
-
meta:一些元数据;
module.exports = function (content, map, meta) {
console.log(content);
return contentl
}
在加载某个模块时,引入loader
注意:传入的路径和context是有关系的,在前面我们讲入口的相对路径时有讲过。
module.exports = {
mode: "development",
devtool: "source-map",
entry: "./src/main.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "./build")
},
module: {
rules: [
// Rule对象
{
test: /\.js$/i,
use: {
loader: "hy-loader01",
options: {
name: "why",
age: "18"
}
}
},
]
},
resolveLoader: {
modules: ["node_modules", "./hy-loaders"]
},
plugins: [
new HtmlWebpackPlugin()
]
}
resolveLoader属性
但是,如果我们依然希望可以直接去加载自己的loader文件夹,有没有更加简洁的办法呢?
- 配置resolveLoader属性;
resolveLoader: {
modules: ["node_modules", "./hy-loaders"]
},
loader的执行顺序
创建多个Loader使用,它的执行顺序是什么呢?
- 从后向前、从右向左的
pitch-loader和enforce
module.exports.pitch = function() {
console.log("loader01 pitch")
}
执行顺序和enforce
其实这也是为什么loader的执行顺序是相反的:
-
run-loader先优先执行PitchLoader,在执行PitchLoader时进行loaderIndex++;
-
run-loader之后会执行NormalLoader,在执行NormalLoader时进行loaderIndex–;
那么,能不能改变它们的执行顺序呢?
- 我们可以拆分成多个Rule对象,通过enforce来改变它们的顺序;
enforce一共有四种方式:
- 默认所有的loader都是normal;
- 在行内设置的loader是inline(在前面将css加载时讲过,import ‘loader1!loader2!./test.js’);
- 也可以通过enforce设置 pre 和 post;
在Pitching和Normal它们的执行顺序分别是:
-
post, inline, normal, pre;
-
pre, normal, inline, post;
同步的Loader
什么是同步的Loader呢?
-
默认创建的Loader就是同步的Loader;
-
这个Loader必须通过 return 或者 this.callback 来返回结果,交给下一个loader来处理;
-
通常在有错误的情况下,我们会使用 this.callback;
this.callback的用法如下:
-
第一个参数必须是 Error 或者 null;
-
第二个参数是一个 string或者Buffer;
module.exports = function(content) {
console.log("loader01", content);
this.callback(null, content)
}
异步的Loader
什么是异步的Loader呢?
-
有时候我们使用Loader时会进行一些异步的操作;
-
我们希望在异步操作完成后,再返回这个loader处理的结果;
-
这个时候我们就要使用异步的Loader了;
loader-runner已经在执行loader时给我们提供了方法,让loader变成一个异步的loader:
module.exports = function(content) {
const callback = this.async()
setTimeout(() => {
console.log("loader01", content)
callback(null, content)
}, 1000)
}
传入和获取参数
在使用loader时,传入参数。
我们可以通过一个webpack官方提供的一个解析库 loader-utils,安装对应的库。
npm install loader-utils -D
{
test: /\.js$/i,
use: {
loader: "hy-loader01",
options: {
name: "why",
age: "18"
}
}
},
const babel = require("@babel/core");
const { getOptions } = require("loader-utils");
module.exports = function(content) {
// 0.设置为异步的loader
const callback = this.async();
// 1.获取传入的参数
const options = getOptions(this);
// 2.对源代码进行转换
babel.transform(content, options, (err, result) => {
if (err) {
callback(err);
} else {
callback(null, result.code)
}
})
}
校验参数
我们可以通过一个webpack官方提供的校验库 schema-utils,安装对应的库:
npm install schema-utils -D
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "请输入您的名字"
},
"age": {
"type": "number",
"description": "请输入您的年龄"
}
},
"additionalProperties": true
}
const { getOptions } = require("loader-utils");
const { validate } = require('schema-utils');
const schema = require("../hy-schema/loader01-schema.json");
// NormalLoader
// 异步Loader: this.async()
module.exports = function(content) {
console.log(content, "哈哈哈, 这是我的loader01");
// 获取传入的参数:
const options = getOptions(this);
console.log("传入的参数是:", options);
validate(schema, options, {
name: "hy-loader02"
})
const callback = this.async();
setTimeout(() => {
callback(null, content);
}, 2000);
}
babel-loader案例
我们知道babel-loader可以帮助我们对JavaScript的代码进行转换,这里我们定义一个自己的babel-loader:
const babel = require("@babel/core");
const { getOptions } = require("loader-utils");
module.exports = function(content) {
// 0.设置为异步的loader
const callback = this.async();
// 1.获取传入的参数
const options = getOptions(this);
// 2.对源代码进行转换
babel.transform(content, options, (err, result) => {
if (err) {
callback(err);
} else {
callback(null, result.code)
}
})
}
{
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"additinalProperties": true
}
hymd-loader
const marked = require('marked');
const hljs = require('highlight.js');
module.exports = function(content) {
marked.setOptions({
highlight: function(code, lang) {
return hljs.highlight(lang, code).value;
}
})
const htmlContent = marked(content);
const innerContent = "`" + htmlContent + "`";
const moduleCode = `var code=${innerContent}; export default code;`
return moduleCode;
}
十、自定义Plugin
1、自定义Plugin
我们知道webpack有两个非常重要的类:Compiler和Compilation
-
他们通过注入插件的方式,来监听webpack的所有生命周期;
-
插件的注入离不开各种各样的Hook,而他们的Hook是如何得到的呢?
-
其实是创建了Tapable库中的各种Hook的实例;
所以,如果我们想要学习自定义插件,最好先了解一个库:Tapable
-
Tapable是官方编写和维护的一个库;
-
Tapable是管理着需要的Hook,这些Hook可以被应用到我们的插件中;
Tapable有哪些Hook呢?
Tapable的Hook分类
同步和异步的:
-
以sync开头的,是同步的Hook;
-
以async开头的,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调;
其他的类别
-
bail:当有返回值时,就不会执行后续的事件触发了;
-
Loop:当返回值为true,就会反复执行该事件,当返回值为undefined或者不返回内容,就退出事件;
-
Waterfall:当返回值不为undefined时,会将这次返回的结果作为下次事件的第一个参数;
-
Parallel:并行,会同时执行次事件处理回调结束,才执行下一次事件处理回调;
-
Series:串行,会等待上一是异步的Hook;
Hook的使用过程
第一步:创建Hook对象
this.hooks = {
syncHook: new SyncHook(["name", "age"])
}
第二步:注册Hook中的事件
this.hooks.syncHook.tap("event1", (name, age) => {
console.log("event1", name, age);
return "event1";
});
this.hooks.syncHook.tap("event2", (name, age) => {
console.log("event2", name, age);
});
第三步:触发事件
emit() {
this.hooks.syncHook.call("why", 18);
}
自定义Plugin
在之前的学习中,我们已经使用了非常多的Plugin:
-
CleanWebpackPlugin
-
HTMLWebpackPlugin
-
MiniCSSExtractPlugin
-
CompressionPlugin
-
等等。。。
这些Plugin是如何被注册到webpack的生命周期中的呢?
-
第一:在webpack函数的createCompiler方法中,注册了所有的插件;
-
第二:在注册插件时,会调用插件函数或者插件对象的apply方法;
-
第三:插件方法会接收compiler对象,我们可以通过compiler对象来注册Hook的事件;
-
第四:某些插件也会传入一个compilation的对象,我们也可以监听compilation的Hook事件;
开发自己的插件
如何开发自己的插件呢?
-
目前大部分插件都可以在社区中找到,但是推荐尽量使用在维护,并且经过社区验证的;
-
这里我们开发一个自己的简单插件:将静态文件自动上传服务器中;
自定义插件的过程:
-
创建AutoUploadWebpackPlugin类;
-
编写apply方法:
通过ssh连接服务器;
删除服务器原来的文件夹;
上传文件夹中的内容;
-
在webpack的plugins中,使用AutoUploadWebpackPlugin类;
十一、Gulp工具
1、Vue脚手架
Vue脚手架启动
Vue脚手架加载配置
vue inspect --mode=development > dev.config.js
vue inspect --mode=production > prod.config.js
2、Gulp
什么是Gulp?
什么是Gulp?
-
A toolkit to automate & enhance your workflow;
-
一个工具包,可以帮你自动化和增加你的工作流;
Gulp和Webpack
gulp的核心理念是task runner
-
可以定义自己的一系列任务,等待任务被执行;
-
基于文件Stream的构建流;
-
我们可以使用gulp的插件体系来完成某些任务;
webpack的核心理念是module bundler
-
webpack是一个模块化的打包工具;
-
可以使用各种各样的loader来加载不同的模块;
-
可以使用各种各样的插件在webpack打包的生命周期完成其他的任务;
gulp相对于webpack的优缺点:
-
gulp相对于webpack思想更加的简单、易用,更适合编写一些自动化的任务;
-
但是目前对于大型项目(Vue、React、Angular)并不会使用gulp来构建,比如默认gulp是不支持模块化的;
Gulp的基本使用
首先,我们需要安装gulp:
# 全局安装
npm install gulp -g
# 局部安装
npm install gulp
其次,编写gulpfile.js文件,在其中创建一个任务:
exports.foo = function() {
console.log("foo task working");
return src("./src/js/*.js")
}
最后,执行gulp命令:
npx gulp foo
创建gulp任务
每个gulp任务都是一个异步的JavaScript函数:
-
此函数可以接受一个callback作为参数,调用callback函数那么任务会结束;
-
或者是一个返回stream、promise、event emitter、child process或observable类型的函数;
任务可以是public或者private类型的:
-
公开任务(Public tasks) 从 gulpfile 中被导出(export),可以通过 gulp 命令直接调用;
-
私有任务(Private tasks) 被设计为在内部使用,通常作为 series() 或 parallel() 组合的组成部分;
补充:gulp4之前, 注册任务时通过gulp.task的方式进行注册的
gulp.task("bar", cb => {
consoele.log("bar任务");
cb();
})
默认任务
我们可以编写一个默认任务:
// 默认任务
exports.default = function(cb) {
console.log("这是一个默认任务")
cb();
}
执行 gulp 命令:
npx gulp
任务组合series和parallel
gulp提供了两个强大的组合方法:
-
series():串行任务组合;
-
parallel():并行任务组合;
-
他们都可以接受任意数量的任务函数或者已经组合的操作;
const seriesTask = series(task1, task2, task3);
const parallelTask = parallel(task1, task2, task3);
const composeTask = series(parallelTask, seriesTask);
读取和写入文件
gulp 暴露了 src() 和 dest() 方法用于处理计算机上存放的文件。
-
src() 接受参数,并从文件系统中读取文件然后生成一个Node流(Stream),它将所有匹配的文件读取到内 存中并通过流(Stream)进行处理;
-
由 src() 产生的流(stream)应当从任务(task函数)中返回并发出异步完成的信号;
-
dest() 接受一个输出目录作为参数,并且它还会产生一个 Node流(stream),通过该流将内容输出到文件中;
exports.default = function() {
return src("./src/index.html").pipe(dest('output/'))
}
流(stream)所提供的主要的 API 是 .pipe() 方法,pipe方法的原理是什么呢?
-
pipe方法接受一个 转换流(Transform streams)或可写流(Writable streams);
-
那么转换流或者可写流,拿到数据之后可以对数据进行处理,再次传递给下一个转换流或者可写流;
对文件进行转换
如果在这个过程中,我们希望对文件进行某些处理,可以使用社区给我们提供的插件。
-
比如我们希望ES6转换成ES5,那么可以使用babel插件;
-
如果我们希望对代码进行压缩和丑化,那么可以使用uglify或者terser插件;
const jsTask = () => {
return src("./src/js/**.js", {base: "./src"})
.pipe(babel({presets: ["@babel/preset-env"]}))
.pipe(terser({mangle: {toplevel: true}}))
.pipe(dest("./dist"))
}
glob文件匹配
src() 方法接受一个 glob 字符串或由多个 glob 字符串组成的数组作为参数,用于确定哪些文件需要被操作。
- glob 或 glob 数组必须至少匹配到一个匹配项,否则 src() 将报错;
glob的匹配规则如下:
- (一个星号*):在一个字符串中,匹配任意数量的字符,包括零个匹配;
'*.js'
- (两个星号**):在多个字符串匹配中匹配任意数量的字符串,通常用在匹配目录下的文件;
'scripts/**/*.js'
-
(取反!):
由于 glob 匹配时是按照每个 glob 在数组中的位置依次进行匹配操作的;
所以 glob 数组中的取反(negative)glob 必须跟在一个非取反(non-negative)的 glob 后面;
第一个 glob 匹配到一组匹配项,然后后面的取反 glob 删除这些匹配项中的一部分;
['scripts/**/*.js', '!scripts/vendor/']
Gulp的文件监听
gulp api 中的 watch() 方法利用文件系统的监控程序(file system watcher)将 与进行关联。
const jsTask = () => {
// 从src中读取文件, 输出到dist文件夹中
return (
src("./src/**/*.js")
.pipe(babel({ presets: ["@babel/preset-env"] }))
// .pipe(uglify())
.pipe(terser({ mangle: { toplevel: true } }))
.pipe(dest("./dist"))
);
};
watch("./src/**/*.js", jsTask);
3、Gulp案例
接下来,我们编写一个案例,通过gulp来开启本地服务和打包:
打包html文件;
打包JavaScript文件;
打包less文件;
Html资源注入
删除生成目录
创建打包任务
创建开发任务
const { src, dest, watch, series, parallel } = require('gulp');
const htmlMin = require('gulp-htmlmin');
const babel = require('gulp-babel');
const terser = require('gulp-terser');
const less = require('gulp-less'); // less
const postcss = require('gulp-postcss'); // postcss
const postcssPresetEnv = require('postcss-preset-env');
const inject = require('gulp-inject');
const browserSync = require('browser-sync');
const del = require('del');
const htmlTask = () => {
return src("./src/*.html", {base: "./src"})
.pipe(htmlMin({
collapseWhitespace: true
}))
.pipe(dest("./dist"))
}
const jsTask = () => {
return src("./src/js/**.js", {base: "./src"})
.pipe(babel({presets: ["@babel/preset-env"]}))
.pipe(terser({mangle: {toplevel: true}}))
.pipe(dest("./dist"))
}
const lessTask = () => {
return src("./src/css/*.less", {base: "./src"})
.pipe(less())
.pipe(postcss([postcssPresetEnv()]))
.pipe(dest("./dist"))
}
const injectHtml = () => {
return src("./dist/*.html")
.pipe(inject(src(["./dist/js/*.js", "./dist/css/*.css"]), {relative: true}))
.pipe(dest('./dist'))
}
// 搭建本地服务器
const bs = browserSync.create();
const serve = () => {
watch("./src/*.html", series(htmlTask, injectHtml));
watch("./src/js/*.js", series(jsTask, injectHtml));
watch("./src/css/*.less", series(jsTask, lessTask));
bs.init({
port: 8080,
open: true,
files: "./dist/*",
server: {
baseDir: "./dist"
}
})
}
const clean = () => {
return del(["dist"])
}
const buildTask = series(clean, parallel(htmlTask, jsTask, lessTask), injectHtml);
const serveTask = series(buildTask, serve);
module.exports = {
serveTask,
buildTask
}
十一、rollup工具
1、rollup
认识rollup
我们来看一下官方对rollup的定义:
-
Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.
-
Rollup是一个JavaScript的模块化打包工具,可以帮助我们编译小的代码到一个大的、复杂的代码中,比如一个库或者一个应用程序;
我们会发现Rollup的定义、定位和webpack非常的相似:
-
Rollup也是一个模块化的打包工具,但是Rollup主要是针对ES Module进行打包的;
-
另外webpack通常可以通过各种loader处理各种各样的文件,以及处理它们的依赖关系;
-
rollup更多时候是专注于处理JavaScript代码的(当然也可以处理css、font、vue等文件);
-
另外rollup的配置和理念相对于webpack来说,更加的简洁和容易理解;
-
在早期webpack不支持tree shaking时,rollup具备更强的优势;
目前webpack和rollup分别应用在什么场景呢?
-
通常在实际项目开发过程中,我们都会使用webpack(比如vue、react、angular项目都是基于webpack的);
-
在对库文件进行打包时,我们通常会使用rollup(比如vue、react、dayjs源码本身都是基于rollup的);
Rollup基本使用
我们可以先安装rollup:
# 全局安装
npm install rollup -g
# 局部安装
npm install rollup -D
创建main.js文件,打包到bundle.js文件中:
# 打包浏览器的库
npx rollup ./src/main.js -f iife -o dist/bundle.js
# 打包AMD的库
npx rollup ./src/main.js -f amd -o dist/bundle.js
# 打包CommonJS的库
npx rollup ./src/main.js -f cjs -o dist/bundle.js
# 打包通用的库(必须跟上name)
npx rollup ./src/main.js -f umd --name mathUtil -o dist/bundle.js
Rollup的配置文件
我们可以将配置信息写到配置文件中rollup.config.js文件:
export default {
input: "./src/main.js",
output: {
format: "cjs",
file: "dist/why.common.js"
}
}
我们可以对文件进行分别打包,打包出更多的库文件(用户可以根据不同的需求来引入):
export default {
input: "./src/main.js",
output: [
{
format: "umd",
name: "whyUtils",
file: "dist/why.umd.js"
},
{
format: "cjs",
file: "dist/why.commonjs.js"
},
{
format: "amd",
file: "dist/why.amd.js"
},
{
format: "es",
file: "dist/why.es.js"
},
{
format: "iife",
name: "whyUtils",
file: "dist/why.browser.js"
}
]
}
解决commonjs和第三方库问题
安装解决commonjs的库:
npm install @rollup/plugin-commonjs -D
安装解决node_modules的库:
npm install @rollup/plugin-node-resolve -D
打包和排除lodash
output: {
format: "umd",
name: "whyUtils",
file: "dist/why.umd.js",
globals: {
lodash: "_",
},
},
Babel转换代码
如果我们希望将ES6转成ES5的代码,可以在rollup中使用babel。
安装rollup对应的babel插件:
npm install @rollup/plugin-babel -D
修改配置文件:
-
注意:babel的位置应该是在commonjs的前面的;
-
需要配置babel.config.js文件;
resolve(),
babel({
exclude: "node_modules/**",
babelHelpers: "bundled",
}),
commonjs(),
module.exports = {
presets: [
"@babel/preset-env",
"@babel/preset-react"
]
}
Teser代码压缩
如果我们希望对代码进行压缩,可以使用rollup-plugin-terser:
npm install rollup-plugin-terser -D
配置terser:
const plugins = [
resolve(),
babel({
exclude: "node_modules/**",
babelHelpers: "bundled",
}),
commonjs(),
terser(),
]
处理css文件
如果我们项目中需要处理css文件,可以使用postcss:
npm install rollup-plugin-postcss postcss -D
配置postcss的插件:
const plugins = [
resolve(),
babel({
exclude: "node_modules/**",
babelHelpers: "bundled",
}),
commonjs(),
terser(),
postcss()
]
处理vue文件
处理vue文件我们需要使用rollup-plugin-vue插件:
- 但是注意:默认情况下我们安装的是vue2.x的版本,所以我这里指定了一下rollup-plugin-vue的版本;
npm install rollup-plugin-vue@4.6.1 vue-template-compiler -D
使用vue的插件:
const plugins = [
resolve(),
babel({
exclude: "node_modules/**",
babelHelpers: "bundled",
}),
commonjs(),
terser(),
postcss(),
vue()
]
打包vue报错
在我们打包vue项目后,运行会报如下的错误:
这是因为在我们打包的vue代码中,用到 process.env.NODE_ENV,所以我们可以使用一个插件 rollup-plugin-replace 设置它对应的值:
npm install rollup-plugin-replace -D
配置插件信息:
const plugins = [
resolve(),
babel({
exclude: "node_modules/**",
babelHelpers: "bundled",
}),
commonjs(),
terser(),
postcss(),
replace({
'process.env.NODE_ENV': JSON.stringify('production')
})
vue()
]
搭建本地服务器
第一步:使用rollup-plugin-serve搭建服务
npm install rollup-plugin-serve -D
serve({
port: 8888,
contentBase: '.'
})
第二步:当文件发生变化时,自动刷新浏览器
npm install rollup-plugin-livereload -D
livereload()
第三步:启动时,开启文件监听
npx rollup -c -w
区分开发环境
我们可以在package.json中创建一个开发和构建的脚本:
十二、vite工具
1、vite
什么是vite呢?
- 官方的定位:下一代前端开发与构建工具;
如何定义下一代开发和构建工具呢?
-
我们知道在实际开发中,我们编写的代码往往是不能被浏览器直接识别的,比如ES6、TypeScript、Vue文件等等;
-
所以我们必须通过构建工具来对代码进行转换、编译,类似的工具有webpack、rollup、parcel;
-
但是随着项目越来越大,需要处理的JavaScript呈指数级增长,模块越来越多;
-
构建工具需要很长的时间才能开启服务器,HMR也需要几秒钟才能在浏览器反应出来;
-
所以也有这样的说法:天下苦webpack久矣;
Vite (法语意为 “快速的”,发音 /vit/) 是一种新型前端构建工具,能够显著提升前端开发体验。
Vite的构造
它主要由两部分组成:
-
一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速;
-
一套构建指令,它使用rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源;
目前是否要大力学习vite?vite的未来是怎么样的?
-
我个人非常看好vite的未来,也希望它可以有更好的发展;
-
但是,目前vite虽然已经更新到2.0,依然并不算非常的稳定,并且比较少大型项目(或框架)使用vite来进行构建;
-
vite的整个社区插件等支持也还不够完善;
-
包括vue脚手架本身,目前也还没有打算迁移到vite,而依然使用webpack(虽然后期一定是有这个打算的);
-
所以vite看起来非常的火热,在面试也可能会问到,但是实际项目中应用的还比较少;
浏览器原生支持模块化
但是如果我们不借助于其他工具,直接使用ES Module来开发有什么问题呢?
-
首先,我们会发现在使用loadash时,加载了上百个模块的js代码,对于浏览器发送请求是巨大的消耗;
-
其次,我们的代码中如果有TypeScript、less、vue等代码时,浏览器并不能直接识别;
事实上,vite就帮助我们解决了上面的所有问题。
Vite的安装
首先,我们安装一下vite工具:
npm install vite -g
通过vite来启动项目:
npx vite
Vite对css的支持
vite可以直接支持css的处理
- 直接导入css即可;
vite可以直接支持css预处理器,比如less
-
直接导入less;
-
之后安装less编译器;
npm install less -D
vite直接支持postcss的转换:
- 只需要安装postcss,并且配置 postcss.config.js 的配置文件即可;
npm install postcss postcss-preset-env -D
module.exports = {
plugins: [
require('postcss-preset-env')
]
}
Vite对TypeScript的支持
vite对TypeScript是原生支持的,它会直接使用ESBuild来完成编译:
- 只需要直接导入即可;
如果我们查看浏览器中的请求,会发现请求的依然是ts的代码:
-
这是因为vite中的服务器Connect会对我们的请求进行转发;
-
获取ts编译后的代码,给浏览器返回,浏览器可以直接进行解析;
注意:在vite2中,已经不再使用Koa了,而是使用Connect来搭建的服务器
Vite对vue的支持
vite对vue提供第一优先级支持:
-
Vue 3 单文件组件支持:@vitejs/plugin-vue
-
Vue 3 JSX 支持:@vitejs/plugin-vue-jsx
-
Vue 2 支持:underfin/vite-plugin-vue2
安装支持vue的插件:
npm install vite-plugin-vue2 -D
在vite.config.js中配置插件:
import { createVuePlugin } from "vite-plugin-vue2";
export default {
plugins: [
createVuePlugin()
]
}
Vite对react的支持
.jsx 和 .tsx 文件同样开箱即用,它们也是通过 ESBuild来完成的编译:
-
所以我们只需要直接编写react的代码即可;
-
注意:在index.html加载main.js时,我们需要将main.js的后缀,修改为 main.jsx 作为后缀名;
Vite打包项目
我们可以直接通过vite build来完成对当前项目的打包工具:
npx vite build
我们可以通过preview的方式,开启一个本地服务来预览打包后的效果:
npx vite preview
Vite脚手架工具
在开发中,我们不可能所有的项目都使用vite从零去搭建,比如一个react项目、Vue项目;
- 这个时候vite还给我们提供了对应的脚手架工具;
所以Vite实际上是有两个工具的:
-
vite:相当于是一个构件工具,类似于webpack、rollup;
-
@vitejs/create-app:类似vue-cli、create-react-app;
如果使用脚手架工具呢?
npm init @vitejs/app
上面的做法相当于省略了安装脚手架的过程:
npm install @vitejs/create-app -g
create-app
ESBuild解析
ESBuild的特点:
-
超快的构建速度,并且不需要缓存;
-
支持ES6和CommonJS的模块化;
-
支持ES6的Tree Shaking;
-
支持Go、JavaScript的API;
-
支持TypeScript、JSX等语法编译;
-
支持SourceMap;
-
支持代码压缩;
-
支持扩展其他插件;
ESBuild的构建速度
ESBuild的构建速度和其他构建工具速度对比:
ESBuild为什么这么快呢?
-
使用Go语言编写的,可以直接转换成机器代码,而无需经过字节码;
-
ESBuild可以充分利用CPU的多内核,尽可能让它们饱和运行;
-
ESBuild的所有内容都是从零开始编写的,而不是使用第三方,所以从一开始就可以考虑各种性能问题;
-
等等
三、知识扩展
1、谷歌浏览器设置跨域
在进行前端开发设置谷歌浏览器跨域时遇到了问题
总结三种方法:
一、49版本以前的设置:
在桌面chrome快捷方式的属性中的目标输入框添加 --disable-web-security 添加部分与前面字符之间有空格(有文章说目标引号结尾的加 --args --disable-web-security,反正我试过,没有49版本之前的)
二、49版本以后的设置:
1.在电脑上新建一个目录,例如:C:\MyChromeDevUserData
2.在属性页面中的目标输入框里加上 --disable-web-security --user-data-dir=C:\MyChromeDevUserData,–user-data-dir的值就是刚才新建的目录(参考上面截图)
(我用此方法能成功设置)
三、如果以上两种方法失败,用以下方法(此方法为网上copy)
1.通过桌面快捷方式打开文件位置,找到谷歌浏览器安装位置,看到有个chrome.exe文件
2.在电脑上新建一个目录,例如:C:\MyChromeDevUserData
3.打开cmd命令行,进入快捷方式位置 例如
cd C:\Program Files (x86)\Google\Chrome\Application
4.通过命令行启动谷歌浏览器
C:\Program Files (x86)\Google\Chrome\Application>chrome.exe --disable-web-security --user-data-dir=C:\MyChromeDevUserData
各位根据自己情况做相应更改即可
2、使用vscode模块化开发加载js类型为module所出现的问题<script type=‘module‘>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./src/index.js" type="module"></script>
</body>
</html>
通过默认浏览器运行时报错,加载js被阻止。出现了以下错误
解决方法如下:
- 下载安装 live server
- 选择需要运行的文件,通过open with live server打开
- 运行成功
3、Module not found: Error: Can’t resolve ‘.\src\main.js’ in XXXX
配置webpack打包主入口和出口
npx webpack --entry .\src\main.js --output-path .\build // 报上面的错误
错误原因:
无法解析Windows的.\src\main.js
路径。需要换成./src/main.js
也就是,改成下面这样就不会报这个错了
npx webpack --entry ./src/main.js --output-path ./build
4、assetModuleType 方式打包 require的图片加载不出来
{
test: /\.(png|jpg|jpe?g|gif|svg)$/,
// type: "asset/resource", // file-loader效果
// type: "asset/inline", // url-loader
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024
}
}
},
const imgEl = new Image();
imgEl.src = require('../img/zznh.png').default
element.appendChild(imgEl)
通过以上方式打包之后的,加载不出zznh.png这张图片
如果换成下面这种引入方式,则可以加载出来
import zznhImage from "../img/zznh.png";
const imgEl = new Image();
// imgEl.src = require('../img/zznh.png').default
imgEl.src = zznhImage
element.appendChild(imgEl)
或者不用assetModuleType。重新使用file-loader那种方式
{
test: /\.(png|jpg|jpe?g|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: "img/[name].[hash:6].[ext]",
// outputPath: 'img'
}
}
]
}
这样子打包也可以正常引入zznh这张图片
5、npx postcss unexpected identifier
一开始,我以为是因为淘宝镜像引起的问题,因为一开始我通过npm安装依赖报错了,就直接用cnpm 安装依赖了
结果npm安装依赖之后的报错更加简便,只有一个
Unexpected identifier
百度也找不到答案,一开始我以为是postcss-cli的版本问题。我一开始的版本是9.1.0,这个时候执行上面那条命令,就只会爆出这个错误。后面我就重新安装postcss-cli
npm uninstall postcss-cli
npm install postcss-cli@8.3.1
之后再执行上述命令行语句之后,这时候的报错才显得正常了起来
ExperimentalWarning: The fs.promises API is experimental
原来是node版本过旧,我原先的node版本为10.16.0,这个版本太老了,重新安装node到14.17.6就一切都正常了
6、CopyWebpackPlugin出现HookWebpackError: Not supported
[webpack-cli] HookWebpackError: Not supported
at xxxxxxxxxxx\node_modules\copy-webpack-plugin\dist\index.js:485:13
.....
-- inner error --
版本问题导致
CopyWebpackPlugin报错版本:10.0.0
解决办法:降低版本
npm i -D copy-webpack-plugin@9.*