likes
comments
collection
share

深入浅出Webpack配置及Babel

作者站长头像
站长
· 阅读数 19

跨域问题

跨域问题和解决方案

1.1.跨域产生的原因

跨域是指去向一个为非本origin(协议、域名、端口任意一个不同)的目标地址发送请求的过程,这样之所以会产生问题是因为浏览器的同源策略限制。看起来同源策略影响了我们开发的顺畅性.实则不然,同源策略存在的必要性之一是为了隔离攻击。

浏览器有一个重要的安全策略,称之为「同源策略」

其中,源=协议+主机+端口源=协议+主机+端口,两个源相同,称之为同源,两个源不同,称之为跨源或跨域

比如:

源 1源 2是否同源
www.baidu.comwww.baidu.com/news
www.baidu.comwww.baidu.com
http://localhost:5000http://localhost:7000
http://localhost:5000http://127.0.0.1:5000
www.baidu.combaidu.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的修改是否在后端允许范围内。 触发预检请求在跨域开发中会碰到的主要情况如下

  1. 首先methods设置 PUTDELETECONNECTOPTIONSTRACE会导致预检请求
  2. 设置了AcceptAccept-LanguageContent-LanguageContent-Type 之外的headers中任一的配置,比如常见的token:authorization,缓存机制cache-contorl
  3. 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的几种方式

  1. 本地代理
  2. nodejs中间件
  3. 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

  1. 用express脚手架生成express模具
npm install express-generator -g
express --view=pug myapp
  1. 设置一个全局路由拦截
app.all('*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
if (req.method == 'OPTIONS') {
  res.send(200);
} else {
  next();
}
})
  1. 再设置对应的代理逻辑
var options = {
  target: 'https://xxxx.xxx.xxx/abc/req',
  changeOrigin: true,
  pathRewrite: (path,req)=>{
    return path.replace('/api','/')
  }
}
app.use('/api', proxy(options));
  1. 进入bin/www中设置对应的端口,或者在process.env.PORT设置port启动值
var port = normalizePort(process.env.PORT || '7002');
  1. 启动脚手架
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';

这几个选项有什么样的区别呢?

深入浅出Webpack配置及Babel

Mode配置代表跟多默认设置

深入浅出Webpack配置及Babel

认识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文件,并且解析,位置十分准确

深入浅出Webpack配置及Babel

不常见的值:

  1. eval-source-map:添加到eval 两数的后面
  2. inline-source-map:添加到文件的后面
  3. cheap-source-map(dev环境):低开销,更加高效
  4. cheap-module-source-map: 和cheap-source-map比如相似,但是对来自Loader的source-map处理的更好
  5. 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)

深入浅出Webpack配置及Babel

Babel的执行阶段:

深入浅出Webpack配置及Babel

webpack和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项目的子包,更加推荐;

深入浅出Webpack配置及Babel