webpack通俗易懂(四)
- 1,source-map
- 2,devServer
- 3, https
- 4,eslint
- 5,git-hooks 与 husky
辛苦编写良久,还望点赞鼓励呦~
1,提高开发效率,完善团队开发规范
1.1,source-map
之前我们通过webpack, 将我们的源码打包成了 bundle.js, 实际上客户端(浏览器)读取的是打包后的 bundle.js, 当浏览器执行代码报错的时候,报错的信息自然也是 bundle 的内容。我们如何将报错信息(bundle错误的语句及其所在行列)映射到源码上,这时候我们就需要用 source-map
了,webpack已经内置了sourcemap功能,我们只需要通过简单的配置,就可以开启它
module.exports = {
// 开启 source map
// 开发中推荐使用 'source-map' // 生产环境一般不开启 sourcemap
devtool: 'source-map'
}
当我们执行打包命令后,我们发现bundle的最后一行总是会多出一个注释,指向打包出的bundle.map.js(sourcemap文件)。 sourcemap文件用来描述源码文件和 bundle 文件的代码位置映射关系。基于它,我们将bundle文件的错误信息映射到源码文件上。
除开'source-map'外,还可以基于我们的需求设置其他值,webpack——devtool一共提供了7种SourceMap模式:
- eval:每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL.
- source-map:生成一个SourceMap文件
- hidden-source-map:和 source-map一样,但不会在 bundle 末尾追加注释
- inline-source-map:生成一个 DataUrl 形式的 SourceMap 文件.
- eval-source-map:每个module会通过eval()来执行,并且生成一个DataUrl形式的 SourceMap
- cheap-source-map:生成一个没有列信息(column-mappings)的SourceMaps文 件,不包含loader的 sourcemap(譬如 babel 的 sourcemap)
- cheap-module-source-map:生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。
要注意的是,生产环境我们一般不会开启sourcemap功能,主要有两点原因:
- 通过bundle和sourcemap文件,可以反编译出源码————也就是说,线上产物有soucemap文件的话,就意味着有暴漏源码的风险。
- 我们可以观察到,sourcemap文件的体积相对比较巨大,这跟我们生产环境的追求不同(生产环境追求更小更轻量的bundle)。
有时候我们期望能第一时间通过线上的错误信息,来追踪到源码位置,从而快速解决掉bug以减轻损失。但又不希望sourcemap文件报漏在生产环境,有什么比较好的方案呢?
1.2,devServer
开发环境下,我们往往需要启动一个web服务,方便我们模拟一个用户从浏览器中访问我们的web服务,读取我们的打包产物,以观测我们的代码在客户端的表现。webpack内置了这样的功能,我们只需要简单的配置就可以开启它。
安装 devServer
yarn add -D webpack-dev-server
devServer.proxy基于强大的中间件 http-proxy-middleware 实现的,因此它支持很多的配置项,我们基于此,可以做应对绝大多数开发场景的定制化配置。
基础使用:
const path = require('path')
devServer: {
static: {
directory: path.join(__dirname, 'dist')
}, // 默认是把dist目录作为web服务的根目录
compress: true, // 可选择开启gzips压缩功能,对应静态资源请求的响应头里的Content-Encoding: gzip
port: 3000, // 端口号
},
为了方便,我们配置一下工程的脚本命令,在package.json的scripts里:
{
"scripts": {
"dev": "webpack serve --mode development"
}
}
- 1.2.1, 添加响应头
有些场景需求下,我们需要为所有响应添加headers, 来对资源的请求和响应打入标志,以便做一些安全防范,或者方便发生异常后做请求的链路追踪。比如:
module.exports = {
devServer: {
headers: {
'X-Token': 'ZlcjLCe+sAW1S4QC8Z'
}
}
}
- 1.2.2, 开启代理
我们打包出的js bundle里有时会含有一些对特定接口的网络请求(ajax/fetch). 要注意,此时客户端地址是在 http://localhost:3000/ 下,假设我们的接口来自 http://localhost:4001/,那制台就会报错跨域,在开发环境下,我们可以使用devServer自带的proxy功能来解决这个问题。
我们新搭建一个服务,在当前项目下新建 server.js:
const http = require('http');
const app = http.createServer((req, res) => {
if (req.url === '/api/user') {
res.end('hello node')
}
})
app.listen(4001, 'localhost', () => {
console.log('localhost listening on 4001')
})
再次打开一个终端执行node server.js,启动服务
浏览器输入:
下面我们开始请求,请求我们可以使用浏览器自带的方法fetch,这个方法返回的是一个promise
fetch('/api/user')
.then(val => val.text()) // res.text()可以把返回的结果变成文本)
.then(res => {
console.log(res)
})
如何解决上面跨域的问题呢:
module.exports = {
//...
devServer: {
proxy: {
'/api': 'http://localhost:4001'
}
}
}
// 如果用户在地址栏一旦请求了一个资源叫 /api 的话,我们就给他指向到 http://localhost:4001 服务器上去
现在对 /api/user 的请求会将请求代理到 http://localhost:4001/api/user。如果不希望传递 /api, 则可以重写路径:
proxy: {
'/api': {
target: 'http://localhost:4001',
pathRewrite: {
'^/api': '/' // 这里可以是'/'也可以是''
}
}
}
- 1.2.3,https
默认情况下,将不接受在 HTTPS 上运行且证书无效的后端服务器。如果想让我们的本地http服务改为https服务,可以这样配置:
devServer: {
https: true
}
重新启动服务:npx webpack, 我们发现访问http://localhost:port是无法访问我们的服务的,我们需要在地址栏里加前缀: https,注意: 由于默认配置使用的是自签名证书,所以有的浏览器会告诉你是不安全的,但我们依然可以继续访问它。当然我们也可以提供自己的证书:
module.exports = {
devServer: {
https: {
cacert: './server.pem',
pfx: './server.pfx',
key: './server.key',
cert: './server.crt',
passphrase: 'webpack-dev-server',
requestCert: true,
}
}
}
- 1.2.4, http2
我们也可以不使用https,可以使用http2
如果想要配置http2,那么直接设置:
devServer: {
http2: true
}
http2默认自带https自签名证书,当然我们仍然可以通过https配置项来使用自己的证书
- 1.2.5, historyApiFallback
如果我们的应用是个SPA(单页面应用),当路由到/some 时(可以直接在地址栏里输入),会发现此时刷新页面后,控制台会报错:
GET http://localhost:3000/some 404 (Not Found)
此时打开network,刷新并查看,就会发现问题所在———浏览器把这个路由当作了静态资源的地址去请求,然而我们并没有打包出/some这样的资源,所以这个访问无疑是404的。如何解决它?我们可以通过配置来提供页面代替任何404的静态资源响应:
module.exports = {
//...
devServer: {
historyApiFallback: true
}
}
此时重启服务刷新后发现请求变成了index.html, 当然, 在多数业务场景下,我们需要根据不同的访问路径定制替代的页面,这种情况下,我们可以使用rewrites这个配置项。 类似这样:
module.exports = {
//...
devServer: {
historyApiFallback: {
rewrites: [
{ from: /^\/$/, to: '/views/landing.html' },
{ from: /^\/subpage/, to: '/views/subpage.html' },
{ from: /./, to: '/views/404.html' },
]
}
}
}
- 1.2.6, 开发服务器主机
如果我们在开发环境中起了一个devserve服务,并希望在同一局域网下的同事也能访问它,只需要配置:
devServer: {
host: '0.0.0.0'
}
这时候,如果我们的同事跟我们处在同一局域网下,就可以通过局域网ip来访问我们的服务 啦
1.3, 模块热替换与热加载
- 模块热替换
模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,
替换、添加或删除 模块,而无需重新加载整个页面
module.exports = {
//...
devServer: {
hot: true,
},
}
HMR 加载样式,如果我们配置了style-loader,那么现在已经同样支持样式文件的
热替换功能了
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
}
这是因为style-loader的实现使用了module.hot.accept,在CSS依赖模块更新之后,
会对 style 标签打补丁。从而实现了这个功能。
热加载(文件更新时,自动刷新我们的服务和页面) 新版的webpack-dev-server
默认已经开启了热加载的功能。 它对应的参数是devServer.liveReload,默认为
true。 注意,如果想要关掉它,要将liveReload设置为false的同时,也要关掉
hot
module.exports = {
//...
devServer: {
liveReload: false, //默认为true,即开启热更新功能。
},
};
1.4,eslint
eslint是用来扫描我们所写的代码是否符合规范的工具。往往我们的项目是多人协作开发的,我们期望统一的代码规范,这时候可以让eslint来对我们进行约束。严格意义上来说,eslint配置跟webpack无关,但在工程化开发环境中,它往往是不可或缺的
yarn add eslint -D
npx eslint --init
我们可以看到控制台里的展示:
并生成了一个配置文件(.eslintrc.json),这样我们就完成了eslint的基本规则配置。 eslint配置文件里的配置项含义如下:
- env 指定脚本的运行环境。每种环境都有一组特定的预定义全局变量。此处使用的 browser 预定义了浏览器环境中的全局变量,es6 启用除了 modules 以外的所有 ECMAScript 6 特性(该选项会自动设置 ecmaVersion 解析器选项为 6)。
- globals 脚本在执行期间访问的额外的全局变量。也就是 env 中未预定义,但我
们又需要使用的全局变量。
- globals 脚本在执行期间访问的额外的全局变量。也就是 env 中未预定义,但我
- extends 检测中使用的预定义的规则集合。
- rules 启用的规则及其各自的错误级别,会合并 extends 中的同名规则,定义冲
突时优先级更高。
- rules 启用的规则及其各自的错误级别,会合并 extends 中的同名规则,定义冲
- parserOptions ESLint 允许你指定你想要支持的 JavaScript 语言选项。
ecmaFeatures 是个对象,表示你想使用的额外的语言特性,这里 jsx 代表启用 JSX。ecmaVersion 用来指定支持的 ECMAScript 版本 。默认为 5,即仅支持 es5,你可以使用 6、7、8、9 或 10 来指定你想要使用的 ECMAScript 版本。你 也可以用使用年份命名的版本号指定为 2015(同 6),2016(同 7),或 2017(同 8)或 2018(同 9)或 2019 (same as 10)。上面的 env 中启用了 es6,自动设置了ecmaVersion 解析器选项为 6。 plugins plugins 是一个 npm 包,通常输出 eslint 内部未定义的规则实现。rules 和 extends 中定义的规则, 并不都在 eslint 内部中有实现。比如 extends 中的 plugin:react/recommended,其中定义了规则开关和等级,但是这些规则如何 生效的逻辑是在其对应的插件 ‘react’ 中实现的
- parserOptions ESLint 允许你指定你想要支持的 JavaScript 语言选项。
新建项目文件夹,并在继承终端中打开:
yarn init -y
npm install eslint -D
npx eslint --init
新建src -> app.js
// app.js
console.log('hello eslit')
npx eslint ./src
// eslintrc.json
{
"rules": {
"no-console": "warn" // 可以在rules中自定义约束规范
}
}
执行npx eslint ./src就可以检测出代码是否存在语法错误等规范问题,我们可以在控制台看出哪些不符合规范,这样并不直观,我们可以通过安装vscode插件eslint醒目的看到代码上有红色波浪线〰️。
我们可以通过命令来让elisnt检测代码——在我们的package.scripts里添加一个脚本命令:
// package.json
{
"scripts": {
"eslint": "eslint ./src"
}
}
然后执行
eslint src
以上我们直观的看到代码规范错误是通过安装vscode插件,如果不想使用插件,又想实时提示报错,我们可以结合 webpack 的打包编译功能来实现。
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node-modules/,
use: ['babel-loader', 'eslint-loader']
}
]
因为我们使用了devServer,因此需要在devServer下添加一个对应的配置参数:
module.exports = {
devServer: {
liveReload: false, //默认为true,即开启热更新功能。
}
}
现在我们就可以实时地看到代码里的不规范报错啦
1.5,git-hooks 与 husky
为了保证团队里的开发人员提交的代码符合规范,我们可以在开发者上传代码时进行校验。我们常用 husky 来协助进行代码提交时的 eslint 校验。在使用husky之前,我们先来研究一下 git-hooks
我们回到项目的根目录下。git init, ls -a 命令 ———— “-a”可以显示隐藏目录(目录名的第一位是.)
接来下我们进入到这个文件夹,进一步查看它内部的内容
cd .git
ls -a
可以看到,当前目录下存在一个hooks文件夹,顾名思义,这个文件夹提供了git 命令相关的钩子
cd hooks
ls -a
那我们可以看到有很多git命令相关的文件名。比如"pre-commit.sample pre- push.sample"。 回到正题——我们期望在git提交(commit)前,对我们的代码进行检测,如果不能通 过检测,就无法提交我们的代码, 这个动作的时机应该是?————"pre commit", 也就是 commit之前。
现在,我们查看一下pre-commit.sample的内容
# cat命令可以查看一个文件的内容
cat pre-commit.sample
OK,它返回了这样的内容,是一串shell注释。翻译过来大概意思是,这是个示例钩子,然后我们看到了这一句话
# To enable this hook, rename this file to "pre-commit"
意思是要启用这个钩子的话,我们就把这个文件的后缀名去掉。
虽然这样对我们本地来讲是可行的,但要注意,.git文件夹的改动无法同步到远端仓库
所以我们期望将git-hook的执行权移交到外面来
好的,我们回到项目的根目录下,然后我们新建一个文件夹,暂时命名为".mygithooks" 然后在此文件夹下,新增一个git-hook文件,命名为"pre-commit",并写入以下内容:
echo pre-commit执行啦
好了,我们新建了自己的git-hook,但此时git并不能识别。下面我们执行这行命令:
# 项目根目录下
git config core.hooksPath .mygithooks
上述命令给我们自己的文件,配置了git-hook的执行权限。
但这个时候我们git commit的话,可能会报这样的waring,并且没有执行我们的
shell:
hint: The 'pre-commit' hook was ignored because it's not set as
executable.
hint: You can disable this warning with `git config
advice.ignoredHook false`
这是因为我们的操作系统没有给出这个文件的可执行权限。
因此我们得再执行这样一句命令:
chmod +x .mygithooks/pre-commit
ok!现在我们尝试执行git add . && git commit -m "any meesage"。 我们发现控制台日志会先打印 “pre-commit执行啦”。 这意味着成功啦!
总结:
也就是说,我们搞git-hook的话,要分三步走:
- 新增任意名称文件夹以及文件pre-commit(这个文件名字比如跟要使用的git- hook名字一致)!
- 执行以下命令来移交git-hook的配置权限
git config core.hooksPath .mygithooks
- 给这个文件添加可执行权限:
chmod +x .mygithooks/pre-commit
然后就成功啦。 这时候我们可以在pre-commit里写任意脚本,比如:
eslint src
当eslint扫描代码,出现error时,会在结束扫描时将退出码设为大于0的数字。 也就是会报错,这时候commit就无法往下执行啦,我们成功的拦截了此次错误操作。
husky
husky在升级到7.x后,做了跟我们上述同样的事。 安装它之前,我们需要在package.json中的script里,先添加
"sctript": {
//...others
"prepare": "husky install"
}
prepare是一个npm钩子,意思是安装依赖的时候,会先执行husky install命令。 这个命令就做了上述的123这三件事! 我们安装了7.x的husky会发现,项目根目录下生成了.husky的文件夹。 当然,7.x的husky似乎是有bug的,如果不能正常使用,那么我们只需要验证两件事:
- 是否移交了git-hook的配置权限?
执行命令 "git config --list"查看core.hooksPath配置是否存在,是否正确指向 了.husky。
如果没有,我们只需要手动的给加上就行:
- 是否移交了git-hook的配置权限?
git config core.hooksPath .husky
- 是否是可执行文件? 参考上述总结中的3即可 这时我们的husky就正常了