section-2.1 什么是loader
loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!
webpack默认只能打包js结尾的文件,因此当需要打包其他格式文件的时候,我们都需要安装对应的loader,并在webpack.config.js里写好配置规则,这里以图片为例
首先先下载file-loader,执行npm i file-loader -D
,然后更改配置如下
module.exports = {
mode: "none",
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [ //模块打包规则
{
test: /\.jpg$/,
use: {
loader: 'file-loader'
}
}
]
}
}
在index.js里引入图片模块
import Header from './header.js'
import Content from './content.js'
import Sidebar from './sidebar.js'
import avatar from './avatar.jpg'
let root = document.getElementById("root")
let header = new Header(root)
let content = new Content(root)
let sidebar = new Sidebar(root)
let img = new Image()
img.src = avatar
root.append(img)
执行npm run bundle
,此时图片就被打包成功。注意,这里的模块返回的是打包后的图片路径,所以可以直接赋值给src。对到webpack,当匹配到.jpg结尾的格式时,会去使用file-loader,file-loader会先把图片移动到dist目录下,并赋予一个唯一的文件名,然后返回图片路径
section-2.2 使用loader打包静态资源(图片篇)
options参数,用来给打包的loader添加额外的规则,代码里写了注释这里就不写太多了
{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'file-loader',
options: {
//placeholder 占位符
name: '[name]_[hash].[ext]', //打包出来的图片名称是原图片名称跟后缀
outputPath: 'images/' //打包到images文件夹下,如果没有会创建该文件夹
}
}
}
这里插点额外知识,[]里除了占位符,还能使用 “:” 来附加规则,如下
{
loader: 'file-loader',
options: {
name: '[sha512:hash:base64:7].[ext]'
}
}
// 个人理解:用sha512这个字符的哈希值转成base64位取前7位,如有误欢迎大佬指正,这部分知识不太懂
gdyb21L.png
除了字符,甚至还能使用函数,来对不同环境制定不同规则:
{
loader: 'file-loader',
options: {
name (file) {
if (env === 'development') {
return '[path][name].[ext]'
}
return '[hash].[ext]'
}
}
}
跟file-loader相似的还有一个url-loader,不同在于url-loader默认返回DataURL,因此需要设置limit来制定规则
{
test: /\.(jpg|png|gif)$/,
use: {
// 如果图片比较小可以使用url-loader
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]', //打包出来的图片名称是原图片名称跟后缀
outputPath: 'images/', //打包到images文件夹下,如果没有会创建该文件夹
limit: 2048 //是否小于2KB,当大于2KB的时候不要打包成DataURL
}
}
}
好处是会把图片返回成base64位文件,省了HTTP请求,但当图片太大的时候,会增加打包后的js文件大小,因此设置好loader,小图片打包成DataURL,大图片依然跟file-url一样打包到images文件夹下
section-2.3 & 2.4 使用 Loader 打包静态资源(样式篇)
打包css,必须使用下面两个loader npm i style-loader css-loader -D
- style-loader:负责把样式添加到head上
- css-loader:负责简析css语法
两者在配置中的顺序不可颠倒,因为webpack解析是从右到左,从下到上,这节的内容这点需要注意,下面不再重复
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
然而现在应该很少会直接写css,而是借助sass/less/stylus这些工具,所以下面以sass为例,先安装sass-loader,sass-loader需要两个包:sass-loader 和 node-sass
由于node-sass比较大,很容易因为网络问题下载不了(因为被墙),当时的报错我就不再特地复现然后截图了,如果安装不了的同学跟着我来就行了,正常都是因为被墙下载不了
- 安装淘宝npm镜像:
npm install -g cnpm --registry=https://registry.npm.taobao.org
- 使用cnpm来安装:
cnpm i sass-loader node-sass -D
虽然我不写options可以直接写成数组,不过这里还是拿官网的来,因为这就是上面说的从下到上,还有注释,根据注释应该也能理解为什么顺序不能颠倒了
{
test: /\.css$/,
/* use: ['style-loader', 'css-loader', 'sass-loader'] */
use: [
{
loader: "style-loader" // 将 JS 字符串生成为 style 节点
},{
loader: "css-loader" // 将 CSS 转化成 CommonJS 模块
},{
loader: "sass-loader" // 将 Sass 编译成 CSS
}
]
}
对的css,我们一般会用 postcss 来给一些支持度还不够好的css3属性加前缀 npm i postcss-loader -D
postcss还需要一份postcss.config.js(又或者命名为.postcssrc.js也行)配置,所以在webpack.config.js同级目录下创建对应的配置
我们需要使用postcss的autoprefixer插件来给css3支持度不够好的属性加前缀,下载插件 npm i autoprefixer -D
这里可能有人跟我一样npm又是下载不了,所以也用cnpm吧:cnpm i autoprefixer -D
,如果你不想每次都去下载插件,也能把postcss所有插件一次性下到node_modules里 npm i postcss-plugin -D
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
如果在sass文件中使用@import引入另外一个sass文件,那么需要让引入进来的sass文件也走一遍postcss和sass-loader进行处理,这个时候我们需要把 css-loader 写成对象,在里面添加 importLoaders 参数(具体的其他参数请查阅官网)
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: "css-loader",
options: {
// 查询参数 importLoaders,用于配置「css-loader 作用于 @import 的资源之前」有多少个 loader。
// 0 => no loaders (default); 1 => postcss-loader; 2 => postcss-loader, sass-loader
importLoaders: 2
}
},
'sass-loader',
'postcss-loader']
}
有些时候,我们并不想css样式全局使用,部分要使用私有化处理(比如vue中给style添加的scope),那么需要在css-loader里添加 modules 参数
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: "css-loader",
options: {
importLoaders: 2,
modules: true // css模块化,这个时候全局的 import index.sass将会失效
}
},
'sass-loader',
'postcss-loader']
}
对应的,index.js中对到sass文件也不能作为全局引入了,需要改写成以下方式
import avatar from './avatar.jpg'
import style from './index.scss'
// import './index.scss' 这种直接无效
let root = document.getElementById("root")
let img = new Image()
img.src = avatar
img.classList.add(style.avatar) // 原来直接写的'avatar'将会失效,具体原因看下面打包后的截图
root.append(img)
可见avatar类名被编译成了_3WpIug-N0UMam_vssd03tO,所以说如果不写 style.avatar 的话,avatar类名将会无定义
对到css中用了字体文件的(或者icon-font),会加载诸如ttf/svg/eot等等这样后缀的名字,这个时候需要使用上面的file-loader,因此为了简单化,图片使用url-loader,字体文件使用file-loader
{
test: /\.(woff|eot|ttf|svg)$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'icon-font/'
}
}
},
{
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 20480 //是否小于20KB
}
}
}
section-2.5 使用 plugins 让打包更便捷
插件(plugins)
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
想要使用一个插件,你只需要 require()
它,然后把它添加到 plugins
数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new
操作符来创建它的一个实例。
为了解决手动把index.html加入到dist目录里,可以使用 html-webpack-plugin 插件来指定默认模板,使用clean-webpack-plugin 来实现每次打包之前删除dist目录。执行npm i html-webpack-plugin clean-webpack-plugin -D
进行安装,webpack.config.js添加plugin参数
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: './index.html' // 以index.html作为html模板
}),
// 打包之前删除dist目录,现在的新版是直接取output路径,不需要自己写删除哪个打包后的文件夹
new CleanWebpackPlugin()
]
Section-2.6 entry 与 output 的基础配置
entry可以支持多入口文件,比方说我们要在页面手动添加jq与逻辑代码
entry: {
jquery: './src/jquery.js',
main: './src/index.js'
}
此时output也需要对应的更改一下,不可将name写固定,否则会因为文件重名而打包失败,必须不写或使用占位符的方式
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist') //__dirname指的是webpack.config.js这个文件所在的路径
}
这样打包出来的文件就会如下:
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="main.js"></script>
真正项目中,我们会给s文件添加cdn域名,这里只作为一个结果展示,所以使用的localhost:8080
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'), //__dirname指的是webpack.config.js这个文件所在的路径
publicPath: 'localhost:8080' //给js添加cdn地址,这里没有所以用这个来看看
}
<script type="text/javascript" src="localhost:8080/jquery.js"></script>
<script type="text/javascript" src="localhost:8080/main.js"></script>
Section-2.7 sourceMap的配置
当我们进行开发的时候,如果代码出现问题,浏览器控制台会进行提示。然而打包后的文件报错行数并不是我们真正开发的文件,这个时候我们就需要使用sourceMap来建立一个映射关系,好让我们知道出问题的实际上是哪一个文件
在webpack.config.js文件中添加devtool参数,此选项控制是否生成,以及如何生成 source map。
module.exports = {
mode: "development",
devtool: 'cheap-module-eval-source-map',
entry: './src/index.js',
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
对到该配置对应的一些关键词:
source-map sourceMap 是一个映射关系,能直接映射到对应的代码文件
cheap 报错不精确到哪一行哪一列,只报错到行
inline 不生成map文件,直接以data64的形式写进打包后的js代码里
module 对到第三方依赖,loader也进行报错
eval 会生成一个eval()函数,把错误写到eval中,性能是最快的
因此,对到开发环境和线上环境,我们需要写不一样的配置参数
development 开发环境一般配置为: devtool: 'cheap-module-eval-source-map'
production 正式环境一般配置为: devtool: 'cheap-module-source-map'
关于sourceMap的实现,可以阅读以下文章:
打破砂锅问到底:详解Webpack中的sourcemap
Introduction to JavaScript Source Maps
JavaScript Source Map 详解
需翻墙的油桶视频
Section-2.8 使用 WebpackDevServer 提升开发效率
当我们每次更改完代码,总需要去执行npm run bundle,这样太麻烦,所以需要让webpack监听打包文件,这个时候我们需要在 package.json里去修改webpack,添加 --watch来监听打包文件,让webpack重新打包
"scripts": {
"bundle": "webpack",
"watch": "webpack --watch"
}
对到本地开发,需要自己去请求json打桩数据,这个时候就必须开启一个服务器,而webpack-dev-server就能为我们构建一个本地服务器
在webpack.config.json里添加 devServer,更多的参数请直接查阅官网,webpack并没有自带webpack-dev-server,所以需要安装 npm i webpack-dev-server -D
devServer: {
// 服务器开启在哪个文件夹下,因为我们output写的是dist,所以在此目录下创建本地服务器
contentBase: './dist',
open: true, // 打包完成后自动打开浏览器
proxy: { // 接口代理,参考另一篇vue模拟去哪儿网的文章,里面去请求的json请求全都走了代理
'/api': 'http://localhost:80'
}
}
此时在package.json里添加一条start命令来启动webpack-dev-server
"scripts": {
"bundle": "webpack",
"watch": "webpack --watch",
"start": "webpack-dev-server"
}
除了在git命令行里运行webpack,还可以在node中使用weback,在以前devServer还不成熟的时候,开发者是直接通过自己写node server来开启本地服务的,当在node里执行webpack的时候,devServer将直接被忽略,所以可以不删除
这里需要借助express 和 webpack-dev-middleware(中间件) 来实现,执行 npm i express webpack-dev-middleware -D
安装这两个node模块
在package.json里创建一个server.js,scripts里添加一条server命令,使用node执行server.js
"scripts": {
"bundle": "webpack",
"watch": "webpack --watch",
"start": "webpack-dev-server",
"server": "node server.js"
}
/* server.js */
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleWare = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
const complier = webpack(config); //webpack编译器,编译器执行一次代码就会打包一次
const app = express();
app.use(webpackDevMiddleWare(complier, {
// 打包输出,因为我不在webpack.config.js里写publicPath,所以这里可以不写
// publicPath: config.output.publicPath
}));
app.listen(3000, () => { // 本地服务器端口号为3000
console.log('server is running');
})
执行npm run server
,打开浏览器 localhost:3000,这个时候也能正常运行webpack打包后的文件,对到devServer里的很多东西,如果想实现还需要自己再添加很多模块,所以一般还是直接使用devServer来运行与配置本地服务
Section-2.9&2.10 Hot Module Replacement 热模块更新
模块热替换 (Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。
热更新是weback的功能,所以需要在webpack.config.js中引入webpack
const webpack = require('webpack');
devServer添加hot参数
devServer: {
contentBase: './dist',
open: true,
proxy: { // 接口代理
'/api': 'http://localhost:80'
},
hot: true, //webpack-dev-server开启热更新
hotOnly: true //html没生效,浏览器不刷新
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
}),
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin() // 前面说了热更新是webpack的功能,所以需要作为插件加进来
]
这样在修改css的时候,页面不会重新刷新,只会重新渲染
但是对到js因为没有loader来帮我们做这些事,所以需要自己进行监听 模块热更新,通过accept来实现
if(module.hot) {
module.hot.accept('./num', () => {
// console.log(num)
// num.reset()
root.removeChild(num.block)
num = new Num();
root.appendChild(num.block)
})
}
class StaticDiv {
constructor () {
this.num = 2000
this.block = document.createElement('div')
this.block.innerHTML = this.num
}
// reset () {
// this.block.innerHTML = this.num
// }
}
export default StaticDiv
这里比较坑爹,个人尝试后发现,上面的注释是没用的,内存里的值还是原来那个,原本我并不想跟文档里一样将这个节点删掉重新加入,但是不行,重新innerHTML也是不行的,如我说的,内存没变,文件怎么修改num的值,index.js还是原来初始化页面的2000,无解,不知道vue底层怎么做的,技术不到位这里只做记录
Section-2.11&2.12 使用 Babel 处理 ES6 语法
当我们写es6的时候,由于需要兼容一些低版本浏览器,所以需要将es6转换为es5,这个时候我们需要使用 babel
跟着官网进行安装即可,进入setup(设置)进行查看
执行npm instal babel-loader @babel/core -D
安装babel
实际上babel并不会对代码进行解析,官网上写的就是将代码直接输出而已,需要解析我们需要使用@babel/preset-env这个插件来解析,执行npm install @babel/preset-env -D
进行安装
单纯这样解析出来的ES5实际上一些低版本浏览器依然不识别,所以还需要使用 babel-polyfill 来进行进行解析
webpack.config.js里添加打包规则
{
test: /\.js$/,
exclude: /node_modules/, // 如果代码是在node-modules,则排除
loader: "babel-loader", //webpack与babel的桥梁,并不会进行解析
options: {
presets: [['@babel/preset-env', { // 解析es6语法
useBuiltIns: 'usage', // @babel/polyfill,用到的语法加入低版本解析,而不是全部加载进去
corejs: 2,
targets: {
chrome: "67" // 打包的目标是兼容chrome67以上,实际如果写这个babel不会解析,因为支持es6语法
}
}]]
}
}
执行npm install --save @babel/polyfill
进行安装
这里我尝试后如果不写corejs的版本,默认会以corejs2.x来(可以看编译时输出的提示),还需要注意,不能使用 --save-dev 去安装polyfill和corejs,因为他们是需要在文件头部引入的(因为这不是编译工具,他是增加拓展,往js里加代码的,我说的应该够直白易懂了吧)
还有,其实我写的是corejs: 3,因为我使用不写或写corejs: 2的时候,打包一直报没有各种模块,即使我删除重装还是一样,不确定是不是core-js版本问题
执行npm install --save @babel/runtime-corejs3
,关于 corejs3的一些问题 可以参考这里,然而对到插件有些新增的我写了以后反而导致 useBuiltIns 无效了,代码打包后变得特别大,所以下面我全都注释掉了
/* webpack.config.js */
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3, // 如果文件引入polyfill反而会出警告
targets: {
chrome: "30" // 故意写很低
}
}]]
}
}
/* index.js */
// import "@babel/polyfill";
// import "core-js/stable";
// import "regenerator-runtime/runtime";
const arr = [
new Promise(() => {}),
new Promise(() => {})
]
arr.map(item => {
consele.log(item)
})
代码转换功能以插件的形式出现,插件是小型的 JavaScript 程序,用于指导 Babel 如何对代码进行转换。你甚至可以编写自己的插件将你所需要的任何代码转换功能应用到你的代码上。例如将 ES2015+ 语法转换为 ES5 语法,我们可以使用诸如 @babel/plugin-transform-arrow-functions 之类的官方插件
Polyfill 是会污染全局环境的(添加全局范围(global scope)和类似 String 这样的内置原型(native prototypes)中),因此我们再项目工程中一般使用 presets,写第三方类库使用 plugin,通过 @babel/plugin-transform-runtime 来实现
需要三个插件
-
npm install --save-dev @babel/plugin-transform-runtime
不会污染全局环境,会以闭包的形式去注入对应的内容 npm install --save @babel/runtime
-
npm install --save @babel/runtime-corejs2
如果corejs改成2,这里需要安装corejs2,这里看不懂,只知道一般需要改成2,没去查阅更多资料
实际中我们可能会在babel写很多配置,因此更好的做法是建立 .babelrc 文件,将 options 挪到该文件中,具体查阅 bable中的配置 ,删除babel的options
// .babelrc
"plugins": [["@babel/plugin-transform-runtime",{
"corejs": 2,
"helps": true,
"regenerator": true,
"useESModules": false
}]]