现代化构建工具

一、兼容性问题的产生

随着时间的推移,es6及其更高版本的'新版js'语法逐渐普及使用开来。
不像后端一样,只需要升级一台掌握在开发者手中的设备(服务器),就万事大吉。
由于客户端浏览器掌握在用户手中,他们拿老的浏览器访问我们新版的语法,代码自然是运行不起来,也就产生了前端颇为头痛的‘兼容’问题。

二、如何解决

新语法与类

对于新版语法(比如新增的语法:for of 、解构、展开运算等等)和对象(比如Set、Promise等等),这些用babel工具来做处理,就可以让老的浏览器也可以跑新的语法代码。

模块化

然而对于前端的模块化,babel却无能为力,因为模块化可是一项大工程,这个时候webpack就登场了,它是专门用于处理模块化的工具。即es6Module或Amd或Cmd等。

编译

babel和webpack原理本质都是:拿到你的代码,然后加工处理后,你再使用它处理过的文件。这个过程叫做编译。

两者关系

这两者相辅相成,都是处理新版js语法工具。
如果你的代码没有包含模块化,那么无须使用webpack,只用babel即可。
如果你的代码没有包含新语法,只包含模块语法如import等,那么无须使用babel,只需使用webpack即可。

准备

安装node:这两者都依赖于node,所以后边的内容,都假设你全局安装好的node环境。
创建项目demo,目录解构如下:

index.html
src
  main.js
package.json  //这个使用npm init -y自动生成

三、babel

babel做什么的?

babel提供将新版的js语法及对象特性。让老的浏览器,比如ie8也支持你写的代码的一款工具。
说通俗了就是es6 转换 es5的工具。

安装babel

npm install --save-dev @babel/core @babel/cli

创建babel配置文件

一般会在根目录下创建配置文件babel.config.js,编译的时候,babel会自动根据配置文件内容智能编译。

// 预设
const presets = [];
// 插件
const plugins = [];
module.exports = { presets,plugins };

插件

你的每一种新语法,对应的就是babel的某一个插件,比如你的箭头函数,对应的是插件包@babel/plugin-transform-arrow-functions
如果你的代码中有用到此语法,那么你需要npm install此插件,并写入plugins的数组中。这样编译的时候,就会调用该插件,对你的代码解析。

预设

预设本质是一个插件集合,比如你写的代码里用到了很多新语法,那么你的配置文件中的plugins就需要写很多,要维护这个插件配置项,很麻烦。
于是预设就出来了。你可以自定义一个预设,但是官方提供的已经够用了,所以一般都用官方的。
一般使用banbel都会用预设,而不用插件,因为它省事。

如果都不设置

如果预设和插件为空,那么执行编译命令时候,那么babel将不会对你的代码做任何处理,输出编译后的代码与你写的一模一样。

编译命令

babel本质上,是它会将你的代码读取,然后根据它的规则编译生成一个新的文件,然后你再引用这个编译后的文件,就可以兼容老浏览器了,编译命令为

npx babel src --out-dir build

src指的是你需要编译的源文件目录,build是编译后生成的目录,如果没有它会自动创建。

babel处理新语法

哪些是新语法,新语法其实是相对而言的,相对于时间或主流或目前大众浏览器兼容性。一般说新语法,指的是es6及其以后的语法。比如:

箭头函数、类、for of、解构和扩展运算、let和const、函数新参默认值、字符串模板、async和await等等
这些新语法跑在老的浏览器上比如ie8,想都不用想,直接就报错了。

举个例子

比如如下代码:

const getUname = ()=>{
    return '你好'
};

分析代码发现用到了两处新语法,const和箭头函数。如果我们用babel编译此代码,需要安装两个对应的解析插件包:

npm install --save-dev @babel/plugin-transform-block-scoping
npm install --save-dev @babel/plugin-transform-arrow-functions

并添加到配置项plugins中:

const presets = [];
const plugins = [
    '@babel/plugin-transform-block-scoping',
    '@babel/plugin-transform-arrow-functions'
];
module.exports = { presets,plugins };

执行前边说的编译命令后,查看build文件夹下的mian.js文件,代码如下:

var getUname = function () {
  return '你好';
};

经过如此手段,再在页面上引入编译后的文件,这代码已经可以再老的浏览器上运行了。

简化配置

也看到了,如果我代码里再有其他新语法,那plugins就会更长了。所以使用预设的方式,即插件集,不再一一手动配置插件项。
安装官方预设:

npm install --save-dev @babel/preset-env

修改配置文件如下:

const presets = [
    '@babel/preset-env'
];
const plugins = [];
module.exports = { presets,plugins };

再来编译,发现输出的跟刚才使用插件项编译的结果一样。

插件或预设的配置项

有一个点要注意,这些插件或预设都是可以配置的。
比如,如上预设项@babel/preset-env我没有给其加配置项,那么默认会编译所有的es6+。
如果你的目标浏览器值考虑chrome某个版本,这样就不划算了。因为会编译好多代码。
这个时候可以添加配置项,为其制定编译的目标浏览器,他就会根据此浏览器当前的兼容性,适当编译,而非全部编译。因为有的代码本身浏览本身已经兼容了某些新语法。

const presets = [
    [
        '@babel/preset-env',
        { // 配置项
            targets: { chrome: "58", ie: "11" }
        }
    ]
];
const plugins = [];
module.exports = { presets,plugins };

babel处理新对象

es6不但扩充了语法,也新增了很多对象,比如:

Promise、Set等等

这些新对象,并非是'语法',所以靠babel编译语法肯定是行不通了,所以babel提供了一个垫片( polyfill)来处理这些新对象。
原理是,引入babel提供的垫片库到全局,这样相当于是在全局(window)注入定义了这些对象。所以就可使用了。
比如我有如下代码

const map = new Map();
map.set('uname','abc')
console.log(map.get('uname'))

通过编译命令编译后,编译过的代码如下:

var map = new Map();
map.set('uname', 'abc');
console.log(map.get('uname'));

可以看到,由于Map是新增的对象,并非语法,所以编译也无法对其转换。这时我们可以通过增加垫片的方式,给当前环境注入这些没有的对象。

安装垫片

npm install --save @babel/polyfill

使用垫片

一般情况下是配合webpack用import的方式引入垫片,但是目前没有学到webpack。所以使用了垫片的另一种引入方式Usage in Browser
在index.html中引入script标签,具体如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div>测试</div>
    <script src="./node_modules/@babel/polyfill/dist/polyfill.js"></script>
    <script src="./src/main.js"></script>
</body>
</html>

这样,即便是ie8,也能支持,打开浏览器,控制台,就可以看到输出了abc。
控制台输入window.Map发现也有了内容,而并非之前的is not defined

babel的其它功能

事实上,babel的野心可不止编译新版js这么简单,它还是支持编译jsx,react、ts等,只要安装了对应的插件或者预设,他就可以编译。

三、webpack

webpack做什么的?

前一章讲babel的时候,我尽量绕开import、export等模块化语法特性,是因为: babel不处理js的模块化。我们还需要一款额外使用模块打包工具。
而webpack恰恰就是这么一款专门处理模块化的库:它是模块打包器,是一种模块化的解决方案。其设计理念,所有资源都是“模块”

它会分析你的项目结构依赖关系,找到Js模块和其它的浏览器不能直接运行的拓展语言如css,TypeScript,并将其转换和打包为合适的格式供浏览器使用。
它只会打包js,其他文件它无法处理,但可以通过合适的loader处理后再给它,它就可以处理了

webpack的核心概念分为 入口(Entry)、加载器(Loader)、插件(Plugins)、出口(Output);

安装webpack

npm install --save-dev webpack webpack-cli

创建webpack配置文件

在 webpack 4 中,可无配置使用,默认入口src>index.js,输出在dist。
然而大多数项目会需要很复杂的设置且在终端手动输入大量命令,这就是为什么 webpack 仍支持配置文件。
在根目录下创建配置文件webpack.config.js

const path = require('path');
module.exports = {
    // 编译模式,开发or生产,若生产,代码会被压缩,调试不利
    mode: "development",
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

编译命令

webpack和babel一样,都是会编译和生成新代码代码。编译命令为

npx webpack

配置文件里指明配置,无须命令行后加参数。

举个例子

项目如下
index.html

<body>
    <div>测试</div>
    <script src="./dist/bundle.js"></script>
</body>

src/untils.js

export var getTime = function(){
   return new Date(); 
};

src/index.js

import { getTime } from  './utils'
console.log(getTime())

执行编译命令,然后浏览器打开页面就可以看到结果了
编译后的文件在配置文件指定的dist目录的bundle.js。
可以明显看到,它把两个文件合打包并成了一个。
经过webpack处理后,浏览器就支持了你的包含模块化的代码

loader

webpack只能打包js文件,对于其他类型文件如ts、css、less、coffeejs。它是不能直接处理的。
需要指定loader,loader处理完后,再由webpack来打包处理就可以了。

对css模块化支持

如果直接import css引入,不进行配置,会报错的。

webpack 在打包文件的时候,发现了 .css 后缀名的文件.
webpack 本身只认识 .js .json 文件,这个 .css 文件,它不知道该怎么处理.
于是就从配置文件中,是否有 moudle.ruls[x].test=/.css$/的配置.发现没有
于是就报错 You may need an appropriate loader to handle this file type

安装相关loader:

npm install --save-dev style-loader css-loader

在module下增加如下配置:

rules: [
   {
      test: /\.css$/,
      use: [ 'style-loader', 'css-loader' ]
   }
 ]

然后新建样式src/index.css

html,body{color: red;}

然后在index.js引入样式:

import './index.css'
console.log('i am hero')

重新编译,刷新页面
可以看到页面上的字已经变红了

用到了两个loader,loader的加载顺序是从右到左:
css-loader: 以字符串形式读取CSS文件。
style-loader:获取字符串,解析这些样式并使用<style>将css-loader内部样式注入html中

plugins

插件比loader更为强大,比如打包优化,压缩,软连接,是对webpack本身的扩展

HtmlWebpackPlugin

每次编译完成后,页面需要手动引入的是编译后的js文件,很麻烦。
而此插件,会自动帮你引入,并生成到编译目录内。
npm install --save-dev html-webpack-plugin

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const htmlWp = new HtmlWebpackPlugin({
    template:'./src/index.html'
});
module.exports = {
    mode: "development",
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [htmlWp]
};

loader与plugins区别

1 .loader在模块加载时的预处理文件,运行在打包文件之前
2 . plugins处理loader无法处理的事,plugins在整个编译周期都起作用

常用功能

  • 语法既有es6新增特性和对象,又有模块
    使用babel-loader
    babel会处理es6除了模块外的其他东西
    然后webpack自己会处理模块
  • 处理ts
    npm npm install --save-dev typescript ts-loader
    根目录下创建配置文件tsconfig.json:
{
    "compilerOptions": {
      "outDir": "./dist/",
      "sourceMap": true,
      "noImplicitAny": false,
      "module": "commonjs",
      "target": "es5",
      "allowJs": true
    }
  }

输出目录、资源映射、是否强制声明类型、编译模块化方案...

  • ts中使用es6语法报错
    使用es6或更高或操作dom的语法,编辑器会标红,不认识。
    需要在tsconfig.js中告诉ts识别、编译额外依赖哪些lib。本来是有默认的,一旦自定义lib,默认就不起作用了,所以要写全一些。
...
"lib": [
       "es5",
       "dom",
       "es2015"
]
...
  • 省略后缀名(.js、.ts)
    在配置文件的配置对象中设置resolve项
resolve: {
        extensions: [".js", ".json",".ts"]
}
  • 不使用框架如何实现npm run dev的效果
    在配置好了插件html-webpack-plugin基础上,还需要再安装如下工具包
npm  install webpack-dev-server

然后在packge.json的script里配置如下即可

"dev": "webpack-dev-server --inline --progress --config webpack.config.js"
posted @ 2023-01-12 11:00  丁少华  阅读(67)  评论(0编辑  收藏  举报