深入浅出Webpack配置及Babel
跨域问题
跨域问题和解决方案
1.1.跨域产生的原因
跨域是指去向一个为非本origin(协议、域名、端口任意一个不同)的目标地址发送请求的过程,这样之所以会产生问题是因为浏览器的同源策略限制。看起来同源策略影响了我们开发的顺畅性.实则不然,同源策略存在的必要性之一是为了隔离攻击。
浏览器有一个重要的安全策略,称之为「同源策略」
其中,源=协议+主机+端口源=协议+主机+端口,两个源相同,称之为同源,两个源不同,称之为跨源或跨域
比如:
源 1 | 源 2 | 是否同源 |
---|---|---|
www.baidu.com | www.baidu.com/news | ✅ |
www.baidu.com | www.baidu.com | ❌ |
http://localhost:5000 | http://localhost:7000 | ❌ |
http://localhost:5000 | http://127.0.0.1:5000 | ❌ |
www.baidu.com | baidu.com | ❌ |
放到同一个服务器是没有跨域的
1.2.跨域问题的解决方案
1.jsonp
最早的解决方案之一就是jsonp,实现方式是通过script标签传递数据,因为script请求不会被同源策略禁止,所以通过script标签去请求跨域数据,并且在script的cb对应func中实现对数据的获取是可行的,当然这种方式需要后端进行配合,后端在前端进行对应请求的时候返回对应的jsonp格式的数据 php案例如下:
<?php
header('Content-type: application/json');
//获取回调函数名
$jsoncallback = htmlspecialchars($_REQUEST ['jsoncallback']);
//json数据
$json_data = '["customername1","customername2"]';
//输出jsonp格式的数据
echo $jsoncallback . "(" . $json_data . ")";
?>
客户端用法如下:
<script type="text/javascript">
function callbackFunction(result, methodName)
{
///result 指向对应数据
}
</script>
<script type="text/javascript" src="http://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction"></script>
2.CORS
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
CORS概念
支持CORS请求的浏览器一旦发现ajax请求跨域,会对请求做一些特殊处理,对于已经实现CORS接口的服务端,接受请求,并做出回应。
有一种情况比较特殊,如果我们发送的跨域请求为“非简单请求”,浏览器会在发出此请求之前首先发送一个请求类型为OPTIONS的“预检请求”,验证请求源是否为服务端允许源,这些对于开发这来说是感觉不到的,由浏览器代理。
总而言之,客户端不需要对跨域请求做任何特殊处理。
简单请求与非简单请求
浏览器对跨域请求区分为“简单请求”与“非简单请求”
“简单请求”满足以下特征:
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:
application/x-www-form-urlencoded、 multipart/form-data、text/plain
不满足这些特征的请求称为“非简单请求”,例如:content-type=applicaiton/json , method = PUT/DELETE...
预检请求
预检请求就是在跨域的时候设置了对应的需要预检的内容,结果上会在普通跨域请求前添加了个options请求,用来检查前端headers的修改是否在后端允许范围内。 触发预检请求在跨域开发中会碰到的主要情况如下
- 首先methods设置 PUT、DELETE、CONNECT、OPTIONS、TRACE会导致预检请求
- 设置了Accept、Accept-Language、Content-Language、Content-Type 之外的headers中任一的配置,比如常见的token:authorization,缓存机制cache-contorl
- Content-Type设置了简单请求不允许的值,如常用的application/json
下面是一段浏览器的JavaScript脚本。
var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send();
上面代码中,HTTP请求的方法是PUT
,并且发送一个自定义头信息X-Custom-Header
。
浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。
OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
"预检"请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字段是Origin
,表示请求来自哪个源。
除了Origin
字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
。
预检请求的回应
那么预检请求我们需要如何处理呢?
预检请求就需要后端设置更多的respones headers了,常用如下:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
上面的HTTP回应中,关键的是Access-Control-Allow-Origin
字段,表示http://api.bob.com
可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
除此之外,后端还需要设置对options请求的判断,我在node中间件中添加的判断如下:
if (req.method == 'OPTIONS') {
res.send(200);
} else {
next();
}
更多详细cors内容可见:developer.mozilla.org/en-US/docs/…
实现CORS的几种方式
- 本地代理
- nodejs中间件
- nginx代理
3.CORS与JSONP的比较
CORS与JSONP的使用目的相同,但是比JSONP更强大。
JSONP只支持GET
请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
1.3 CORS实现方式
本地代理
在dva中的实现方式是在.webpackrc中添加如下代码
"proxy": {
"/api": {
"target": "http://127.0.0.1:8988/",
"changeOrigin": true,
"pathRewrite": { "^/api" : "" }
}
}
/api代表代理的路径名,target代表代理的地址,changeOrigin代表更改发出源地址为target,pathRewrite代表路径重写,别的脚手架直接加载webpack配置文件即可
nodejs跨域中间件
具体实现过程我是使用express+http-proxy-middleware
- 用express脚手架生成express模具
npm install express-generator -g
express --view=pug myapp
- 设置一个全局路由拦截
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
if (req.method == 'OPTIONS') {
res.send(200);
} else {
next();
}
})
- 再设置对应的代理逻辑
var options = {
target: 'https://xxxx.xxx.xxx/abc/req',
changeOrigin: true,
pathRewrite: (path,req)=>{
return path.replace('/api','/')
}
}
app.use('/api', proxy(options));
- 进入bin/www中设置对应的端口,或者在process.env.PORT设置port启动值
var port = normalizePort(process.env.PORT || '7002');
- 启动脚手架
DEBUG=myapp:* npm start
启动代理后就可以直接在对应的项目中请求中间件实现跨域了
Nginx跨域代理
Nginx是国外大神实现用的用于反向代理的异步web服务器 他除了用于反向代理以外还可以用于负载均衡、HTTP缓存 接下来来介绍安装方式 首先安装Nginx要安装Homebrew, 然而我的mac并没有Homebrew,那么还需要先用Ruby安装一下这个包管理器
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
查看下brew是否安装成功,成功后可以顺利的安装Nginx啦
brew -v
brew install nginx
nginx -v
Nginx的常用命令有这些
#查看版本,以及配置文件地址
nginx -V
#查看版本
nginx -v
#指定配置文件
nginx -c filename
#帮助
nginx -h
#重新加载配置|重启|停止|退出 nginx
nginx -s reload|reopen|stop|quit
#打开 nginx
sudo nginx
#测试配置是否有语法错误
sudo nginx -t
然后就要进入Nginx配置文件
sudo vim /usr/local/etc/nginx/nginx.conf
ps:nginx-http常见配置项如下
http {
#导入类型配置文件
include mime.types;
#设定默认类型为二进制流
default_type application/octet-stream;
#启用sendfile()函数
sendfile on;
#客户端与服务器连接的超时时间为65秒,超过65秒,服务器关闭连接
keepalive_timeout 65;
#是否开启gzip,默认关闭
#gzip on;
#一个server块
server {
#服务器监听的端口为80
listen 80;
#服务器名称为localhost,我们可以通过localhost来访问这个server块的服务
server_name localhost;
#location块,它存放在server块当中,location会尝试根据用户请求中的URI来匹配上面的/uri表达式,如果可以匹配,就选择location {}块中的配置来处理用户请求。
location / {
#以root方式设置资源路径,它与alias的不同请见下面的 http模块中文件路径定义
root html;
#默认访问的页面,从左依次找到右,直到找到这个文件,然后返回结束请求
index index.html index.htm;
#设置错误页面,对应的错误码是404,错误页面是/Users/user/Sites/404.html
error_page 404 /404.html;
}
}
include servers/*;
}
接下来就是重要的实现反向代理的方式了 这里介绍的主要展示方式是如何在线上设置代理,所以代理的入口是静态资源
server {
listen 80;
server_name localhost;
location / {
root /Users/abc/dist/;
index index.html index.htm;
}
location /api/ {
proxy_pass https://xxx.xxx.xxx/req/;
}
}
location中的后的内容会尝试根据用户请求中的URI来匹配上面的/uri表达式,如果可以匹配,就选择location {}块中的配置来处理用户请求
本项目中第一个location用于指向静态资源位置 root:目录,index:入口文件,第二个location用于进行api的跨域指向
如果你想要对不同的端口实现代理,可以设置多个server listen同一个端口,根据server_name判断请求来源,根据location 设置代理去向
webpack配置
Mode配置
Mode配置选项,可以告知webpack使用相应模式的内置优化:
- 默认值是production (什么都不设置的情况下) ;
- 可选值有: 'none'| 'development'| production';
这几个选项有什么样的区别呢?
Mode配置代表跟多默认设置
认识source-map
我们的代码通常运行在浏览器上时,是通过打包压缩的:
- 也就是真实跑在浏览器.上的代码,和我们编写的代码其实是有差异的;
- 比如ES6的代码可能被转换成ES5;
- 比如对应的代码行号、列号在经过编译后肯定会不一致;
- 比如代码进行丑化压缩时,会将编码名称等修改;
- 比如我们使用了TypeScript等方式编写的代码,最终转换成JavaScript;
但是,当代码报错需要调试时(debug) ,调试转换后的代码是很困难的
但是我们能保证代码不出错吗?不可能。
那么如何可以调试这种转换后不一致的代码呢?答案就是source-map
- source-map是从已转换的代码,映射到原始的源文件;
- 使浏览器可以重构原始源并在调试器中显示重建的原始源;
如何使用source-map
如何可以使用source-map呢?两个步骤:
-
第一步:根据源文件,生成source-map文件,webpack在打包时, 可以通过配置生成source-map;
-
第二步:在转换后的代码,最后添加一一个注释,它指向sourcemap;
//# sourceMappingURL=bundle.js.map
浏览器会根据我们的注释,查找相应的source-map, 并且根据source-map还原我们的代码,方便进行调试。
分析source-map
最初source-map生成的文件大小是原始文件的10倍,第二二版减少了约50%,第三版又减少了50%,所以目前一个133kb的文件,最终的source-map的大小大概在300kb。
目前的source-map长什么样子呢?
- version:当前使用的版本,也就是最新的第三版;
- sources:从哪些文件转换过来的source-map和打包的代码(最初始的文件) ;
- names:转换前的变量和属性名称(因为我目前使用的是development模式,所以不需要保留转换前的名称) ;
- mappings: source-map用来和源文件映射的信息(比如位置信息等),一串base64 VLQ (veriable-length quantity可变长度值)编码;
- file:打包后的文件(浏览器加载的文件) ;
- sourceContent:转换前的具体代码信息(和sources是对应的关系) ;
- sourceRoot:所有的sources相对的根目录;
{
"version": 3,
"file": "bundle.js",
"mappings": "wCAAA,SAASA,EAAIC,EAAMC,GACjB,OAAOD,EAAOC,CAChB,C,4BCDIC,EAA2B,CAAC,EAGhC,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqBE,IAAjBD,EACH,OAAOA,EAAaE,QAGrB,IAAIC,EAASN,EAAyBE,GAAY,CAGjDG,QAAS,CAAC,GAOX,OAHAE,EAAoBL,GAAUI,EAAQA,EAAOD,QAASJ,GAG/CK,EAAOD,OACf,CCrBAJ,EAAoBO,EAAI,CAACH,EAASI,KACjC,IAAI,IAAIC,KAAOD,EACXR,EAAoBU,EAAEF,EAAYC,KAAST,EAAoBU,EAAEN,EAASK,IAC5EE,OAAOC,eAAeR,EAASK,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDT,EAAoBU,EAAI,CAACK,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFhB,EAAoBoB,EAAKhB,IACH,oBAAXiB,QAA0BA,OAAOC,aAC1CX,OAAOC,eAAeR,EAASiB,OAAOC,YAAa,CAAEC,MAAO,WAE7DZ,OAAOC,eAAeR,EAAS,aAAc,CAAEmB,OAAO,GAAO,E,MCL9D,MAAM,IAAE3B,GAAQ,EAAQ,KAGxB4B,QAAQC,IADQ,eAGhBD,QAAQC,IAAIC,SAEZF,QAAQC,IAAI7B,EAAI,GAAI,KAGlB4B,QAAQC,IAAI,e",
"sources": [
"webpack://01-source-map/./src/utils/math.js",
"webpack://01-source-map/webpack/bootstrap",
"webpack://01-source-map/webpack/runtime/define property getters",
"webpack://01-source-map/webpack/runtime/hasOwnProperty shorthand",
"webpack://01-source-map/webpack/runtime/make namespace object",
"webpack://01-source-map/./src/main.js"
],
"sourcesContent": [
"function add(num1, num2) {\r\n return num1 + num2\r\n}\r\n\r\n\r\nexport {\r\n add\r\n}",
"// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n",
"// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};"
],
"names": [
"add",
"num1",
"num2",
"log",
"address"
],
"sourceRoot": ""
}
生成source-map
如何在使用webpack打包的时候选择不同的devtool,可以生成对相应的source-map
- webpack为我们提供了非常多的选项(目前是26个),来处理source-map; .
- webpackdocschina.org/configurati…
- 选择不同的值,生成的source-map会稍微有差异, 打包的过程也会有性能的差异,可以根据不同的情况进行选择;
下面几个值不会生成source-map
-
false: 不使用source-map, 也就是没有任何和source-map相关的内容。
-
none: production模式下的默认值(什么值都不写), 不生成source-map.
-
eval: development模式下的默认值, 不生成source-map的
- 但是它会在eval执行的代码中,添加//# sourceURL=;
- 它会被浏览器在执行时解析,并且在调试面板中生成对应的一些文件目录,方便我们调试代码;
- 但是它生成的文件位置可能会不那么准确
如果选择devtool: "source-map"
-
生成一一个独立的source-map文件,并且在bundle文件中有一个注释,指向source-map文件;
-
bundle文件中有如下的注释:
//# sourceMappingURL=bundle.js.map
-
开发工具会根据这个注释找到source-map文件,并且解析,位置十分准确
不常见的值:
- eval-source-map:添加到eval 两数的后面
- inline-source-map:添加到文件的后面
- cheap-source-map(dev环境):低开销,更加高效
- cheap-module-source-map: 和cheap-source-map比如相似,但是对来自Loader的source-map处理的更好
- hidden-source-map:会生成sourcemap文件,但是 不会对source map文件进行引用
深入浅出Babel
为什么需要Babel
事实上,在开发中我们很少直接去接触babel,但是babel对于前端开发来说,目前是不可缺少的-部分:
- 开发中,我们想要使用ES6+的语法,想要使用TypeScript, 开发React项目,它们都是离不开Babel的;
- 所以,学习Babel对于我们理解代码从编写到线上的转变过程至关重要;
那么,Babel到底是什么呢?
- Babel是一个工具链, 主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript;
- 包括:语法转换、源代码转换、Polyfill实现目 标环境缺少的功能等;
Babel命令行使用
babel本身可以作为一个独立的工具(和postcss一样) ,不和webpack等构建工具配置来单独使用。 如果我们希望在命令行尝试使用babel,需要安装如下库:
- @babel/core: babel的核心代码,必须安装;
- @babel/cli:可以让我们在命令行使用babel;
npm install @babel/cli @babel/core
使用babel来处理我们的源代码: .
-
src:是源文件的目录;
-
--out-dir:指定要输出的文件夹dist;
npx babel ./src --out-dir ./dist
比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件:
npm install @babel/plugin-transform-arrow-functions
npx babel ./src --out-dir ./dist --plugins=@babel/plugin-transform-arrow-functions
查看转换后的结果:我们会发现const并没有转成var
- 这是因为plugin-transform-arrow-functions,并没有提供这样的功能;
- 我们需要使用plugin-transform-block-scoping 来完成这样的功能;
npm install @babel/plugin-transform-block-scoping
npx babel ./src --out-dir ./dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
Babel的预设
但是如果要转换的内容过多,-个个设置是比较麻烦的,我们可以使用预设(preset) :
- 后面我们再具体来讲预设代表的含义;
安装@babel/preset-env预设:
npm install @babel/preset-env -D
执行如下命令:
npx babel ./src --out-dir ./dist --presets=@babel/preset-env
Babel的底层原理
babel是如何做到将我们的一段代码(ES6. TypeScript. React) 转成另外一段代码(ES5) 的呢?
- 从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?
- 就是编译器,事实上我们可以将babel看成就是一个编译器。
- Babel编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码;
Babel也拥有编译器的工作流程:
- 解析阶段(Parsing)
- 转换阶段(Transformation)
- 生成阶段(Code Generation)
Babel的执行阶段:
webpack和babel
- webpack是模块化内容
- babel是代码转化内容
在webpack中使用babel
在实际开发中,我们通常会在构建工具中通过配置babel来对其进行使用的,比如在webpack中。
那么我们就需要去安装相关的依赖:
- 如果之前已经安装了@babel/core,那么这里不需要再次安装;
npm install babel-loader @babel/core
我们可以设置-个规则,在加载js文件时,使用我们的babel:
const path = require("path")
module.exports = {
// 设置环境模式
mode: "development",
devtool: false,
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "bundle.js",
// 重新打包时,现将之前打包的文件夹删掉
clean: true
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: "babel-loader",
// 使用插件
options: {
// plugins: [
// "@babel/plugin-transform-arrow-functions",
// "@babel/plugin-transform-block-scoping"
// ]
// 使用预设
presets: [
"@babel/preset-env"
]
}
}
}
]
}
}
babel-preset
如果我们一个个去安装使用插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并且将其传递给babel。
比如常见的预设有三个:
- env
- react
- TypeScript
Babel的配置文件
像之前一样,我们可以将babel的配置信息放到-个独立的文件中,babel给我们提供了两种配置文件的编写:
- babel.config.json (或者js, .cjs, .mjs) 文件; .
- .babelrc.json (或者babelrc, js, .cjs, .mjs) 文件;
它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel本身、 element-plus. umi等) ;
- .babelrc.json: 早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的;
- babel.config.json (babel7) :可以直接作用于Monorepos项目的子包,更加推荐;
转载自:https://juejin.cn/post/7252678036686946359