Webpack实战——使用新语言
使用ES6语言
虽然目前部分浏览器和Node.js已经支持ES6,但由于它们对ES6所有的标准支持不全,这导致在开发中不敢全面地使用ES6。
通常我们需要把采用ES6编写的代码转换成目前已经支持良好的ES5代码,这包含2件事:
- 把新的ES6语法用ES5实现,例如ES6的
class
语法用ES5的prototype
实现。 - 给新的API注入
polyfill
,例如项目使用fetch API
时,只有注入对应的polyfill
后,才能在低版本浏览器中正常运行。
Babel
Babel可以方便的完成以上2件事。 Babel是一个JavaScript编译器,能将ES6代码转为ES5代码,让你使用最新的语言特性而不用担心兼容性问题,并且可以通过插件机制根据需求灵活的扩展。 在Babel执行编译的过程中,会从项目根目录下的.babelrc
文件读取配置。.babelrc
是一个JSON格式的文件,内容大致如下:
{
"plugins": [
[
"transform-runtime",
{
"polyfill": false
}
]
],
"presets": [
[
"es2015",
{
"modules": false
}
],
"stage-2",
"react"
]
}
Plugins
plugins
属性告诉Babel要使用哪些插件,插件可以控制如何转换代码。
以上配置文件里的transform-runtime
对应的插件全名叫做babel-plugin-transform-runtime
,即在前面加上了babel-plugin-
,要让Babel正常运行我们必须先安装它:
npm i -D babel-plugin-transform-runtime
babel-plugin-transform-runtime
是 Babel官方提供的一个插件,作用是减少冗余代码。 Babel在把ES6代码转换成ES5代码时通常需要一些ES5写的辅助函数来完成新语法的实现,例如在转换class extent
语法时会在转换后的ES5代码里注入 _extent
辅助函数用于实现继承:
function _extent(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
}
这会导致每个使用了class extent
语法的文件都被注入重复的_extent
辅助函数代码,babel-plugin-transform-runtime
的作用在于不把辅助函数内容注入到文件里,而是注入一条导入语句:
var _extent = require('babel-runtime/helpers/_extent');
这样能减小Babel编译出来的代码的文件大小。
同时需要注意的是由于babel-plugin-transform-runtime
注入了require('babel-runtime/helpers/_extent')
语句到编译后的代码里,需要安装babel-runtime
依赖到你的项目后,代码才能正常运行。 也就是说babel-plugin-transform-runtime
和babel-runtime
需要配套使用,使用了 babel-plugin-transform-runtime
后一定需要babel-runtime
。
Presets
presets
属性告诉 Babel 要转换的源码使用了哪些新的语法特性,一个 Presets 对一组新语法特性提供支持,多个 Presets 可以叠加。 Presets 其实是一组 Plugins 的集合,每一个 Plugin 完成一个新语法的转换工作。Presets 是按照 ECMAScript 草案来组织的,通常可以分为以下三大类:
-
已经被写入 ECMAScript 标准里的特性,由于之前每年都有新特性被加入到标准里,所以又可细分为:
- es2015 包含在2015里加入的新特性;
- es2016 包含在2016里加入的新特性;
- es2017 包含在2017里加入的新特性;
- env 包含当前所有 ECMAScript 标准里的最新特性。
它们之间的关系如图:
- 被社区提出来的但还未被写入ECMAScript标准里特性,这其中又分为以下四种:
它们之间的关系如图:
- 为了支持一些特定应用场景下的语法,和ECMAScript标准没有关系,例如
babel-preset-react
是为了支持React开发中的JSX语法。
在实际应用中,你需要根据项目源码所使用的语法去安装对应的 Plugins 或 Presets。
接入Babel
由于Babel所做的事情是转换代码,所以应该通过Loader
去接入Babel,Webpack配置如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
},
]
},
// 输出 source-map 方便直接调试 ES6 源码
devtool: 'source-map'
};
配置命中了项目目录下所有的JavaScript文件,通过babel-loader
去调用Babel完成转换工作。 在重新执行构建前,需要先安装新引入的依赖:
# Webpack 接入 Babel 必须依赖的模块
npm i -D babel-core babel-loader
# 根据你的需求选择不同的 Plugins 或 Presets
npm i -D babel-preset-env
使用TypeScript语言
TypeScript是JavaScript的一个超集,主要提供了类型检查系统和对ES6语法的支持,但不支持新的API。 目前没有任何环境支持运行原生的TypeScript代码,必须通过构建把它转换成JavaScript代码后才能运行。
改造下前面用过的例子 Hello,Webpack
,用TypeScript重写JavaScript。由于TypeScript是JavaScript的超集,直接把后缀.js
改成.ts
是可以的。 但为了体现出TypeScript的不同,我们重写JavaScript代码为如下,加入类型检查:
// show.ts
// 操作 DOM 元素,把 content 显示到网页上
// 通过 ES6 模块规范导出 show 函数
// 给 show 函数增加类型检查
export function show(content: string) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// main.ts
// 通过 ES6 模块规范导入 show 函数
import {show} from './show';
// 执行 show 函数
show('Webpack');
TypeScript官方提供了能把TypeScript转换成JavaScript的编译器。 你需要在当前项目根目录下新建一个用于配置编译选项的tsconfig.json
文件,编译器默认会读取和使用这个文件,配置文件内容大致如下:
{
"compilerOptions": {
"module": "commonjs", // 编译出的代码采用的模块规范
"target": "es5", // 编译出的代码采用 ES 的哪个版本
"sourceMap": true // 输出 Source Map 方便调试
},
"exclude": [ // 不编译这些目录里的文件
"node_modules"
]
}
通过npm install -g typescript
安装编译器到全局后,你可以通过tsc hello.ts
命令编译出 hello.js
和 hello.js.map
文件。
减少代码冗余
TypeScript编译器会有和Babel一样的问题:在把ES6语法转换成ES5语法时需要注入辅助函数, 为了不让同样的辅助函数重复的出现在多个文件中,可以开启TypeScript编译器的importHelpers
选项,修改tsconfig.json
文件如下:
{
"compilerOptions": {
"importHelpers": true
}
}
该选项的原理和babel-plugin-transform-runtime
非常类似,会把辅助函数换成如下导入语句:
var _tslib = require('tslib');
_tslib._extend(target);
这会导致编译出的代码依赖tslib
这个迷你库,但避免了代码冗余。
集成Webpack
要让Webpack支持TypeScript,需要解决以下2个问题:
- 通过
Loader
把TypeScript转换成JavaScript。 - Webpack在寻找模块对应的文件时需要尝试
ts
后缀。
对于问题1,社区已经出现了几个可用的Loader
,推荐速度更快的awesome-typescript-loader。 对于问题2,我们需要修改默认的resolve.extensions
配置项。相关Webpack配置如下:
const path = require('path');
module.exports = {
// 执行入口文件
entry: './main',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
},
resolve: {
// 先尝试 ts 后缀的 TypeScript 源码文件
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
loader: 'awesome-typescript-loader'
}
]
},
devtool: 'source-map',// 输出 Source Map 方便在浏览器里调试 TypeScript 代码
};
在运行构建前需要安装上面用到的依赖:
npm i -D typescript awesome-typescript-loader
安装成功后重新执行构建,你将会在dist
目录看到输出的JavaScript 文件bundle.js
,和对应的Source Map文件bundle.js.map
。 在浏览器里打开index.html
页面后,来开发工具里可以看到和调试用TypeScript编写的源码。
使用Flow检查器
认识 Flow
Flow是一个Facebook开源的JavaScript静态类型检测器,它是JavaScript语言的超集。 你所需要做的就是在需要的地方加上类型检查,例如在两个由不同人开发的模块对接的接口出加上静态类型检查,能在编译阶段就指出部分模块使用不当的问题。 同时Flow也能通过类型推断检查出JavaScript代码中潜在的Bug。
Flow使用效果如下:
// @flow
// 静态类型检查
function square1(n: number): number {
return n * n;
}
square1('2'); // Error: square1 需要传入 number 作为参数
// 类型推断检查
function square2(n) {
return n * n; // Error: 传入的 string 类型不能做乘法运算
}
square2('2');
需要注意的时代码中的第一行 // @flow
告诉 Flow 检查器这个文件需要被检查。
使用Flow
以上只是让你了解Flow的功能,下面教你如何运行Flow去检查代码。 Flow检测器由高性能跨平台的OCaml语言编写,它的可执行文件可以通过
npm i -D flow-bin
安装,安装完成后通过先配置Npm Script
"scripts": {
"flow": "flow"
}
再通过npm run flow
去调用Flow执行代码检查。
除此之外还可以通过
npm i -g flow-bin
把Flow安装到全局后,再直接通过flow
命令去执行代码检查。
安装成功后,在项目根目录下执行Flow后,Flow会遍历出所有需要检查的文件并对其进行检查,输出错误结果到控制台,例如:
Error: show.js:6
6: export function show(content) {
^^^^^^^ parameter `content`. Missing annotation
Found 1 error
采用了Flow静态类型语法的JavaScript是无法直接在目前已有的JavaScript引擎中运行的,要让代码可以运行需要把这些静态类型语法去掉。 例如:
// 采用 Flow 的源代码
function foo(one: any, two: number, three?): string {}
// 去掉静态类型语法后输出代码
function foo(one, two, three) {}
有两种方式可以做到这点:
- flow-remove-types 可单独使用,速度快。
- babel-preset-flow 与 Babel 集成。
集成Webpack
由于使用了Flow项目一般都会使用ES6语法,所以把Flow集成到使用Webpack构建的项目里最方便的方法是借助Babel。加入Flow代码检查,如下:
- 安装
npm i -D babel-preset-flow
依赖到项目。 - 修改
.babelrc
配置文件,加入Flow Preset:
"presets": [
...[],
"flow"
]
往源码里加入静态类型后重新构建项目,你会发现采用了Flow的源码还是能正常在浏览器中运行。
要明确构建的目的只是为了去除源码中的Flow静态类型语法,而代码检查和构建无关。 许多编辑器已经整合Flow,可以实时在代码中高亮指出Flow检查出的问题。
使用SCSS语言
认识 SCSS
SCSS可以让你用更灵活的方式写CSS。 它是一种CSS预处理器,语法和CSS相似,但加入了变量、逻辑、等编程元素,代码类似这样:
$blue: #1875e7;
div {
color: $blue;
}
SCSS又叫SASS,区别在于SASS语法类似Ruby,而SCSS语法类似CSS,对于熟悉CSS的前端工程师来说会更喜欢 SCSS。
采用SCSS去写CSS的好处在于可以方便地管理代码,抽离公共的部分,通过逻辑写出更灵活的代码。 和SCSS类似的CSS预处理器还有LESS 等。
使用SCSS可以提升编码效率,但是必须把SCSS源代码编译成可以直接在浏览器环境下运行的CSS代码。 SCSS官方提供了多种语言实现的编译器,由于本书更倾向于前端工程师使用的技术栈,所以主要来介绍下 node-sass。
node-sass
核心模块是由C++编写,再用Node.js封装了一层,以供给其它Node.js调用。node-sass
还支持通过命令行调用,先安装它到全局:
npm i -g node-sass
再执行编译命令:
# 把 main.scss 源文件编译成 main.css
node-sass main.scss main.css
你就能在源码同目录下看到编译后的main.css
文件。
接入Webpack
由于需要把SCSS源代码转换成CSS代码,转换文件最适合的方式是使用Loader
,Webpack官方提供了对应的sass-loader。
Webpack接入sass-loader
相关配置如下:
module.exports = {
module: {
rules: [
{
// 增加对 SCSS 文件的支持
test: /\.scss/,
// SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
use: ['style-loader', 'css-loader', 'sass-loader'],
},
]
},
};
以上配置通过正则/\.scss/
匹配所有以.scss
为后缀的SCSS文件,再分别使用3个Loader
去处理。具体处理流程如下:
- 通过
sass-loader
把SCSS源码转换为CSS代码,再把CSS代码交给css-loader
去处理。 -
css-loader
会找出CSS代码中的@import
和url()
这样的导入语句,告诉Webpack依赖这些资源。同时还支持CSS Modules、压缩CSS等功能。处理完后再把结果交给style-loader
去处理。 -
style-loader
会把CSS代码转换成字符串后,注入到JavaScript代码中去,通过JavaScript去给DOM增加样式。如果你想把CSS代码提取到一个单独的文件而不是和JavaScript混在一起,可以使用ExtractTextPlugin
。
由于接入sass-loader
,项目需要安装这些新的依赖:
# 安装 Webpack Loader 依赖
npm i -D sass-loader css-loader style-loader
# sass-loader 依赖 node-sass
npm i -D node-sass
使用PostCSS
认识 PostCSS
PostCSS是一个CSS处理工具,和SCSS不同的地方在于它通过插件机制可以灵活的扩展其支持的特性,而不是像 SCSS 那样语法是固定的。 PostCSS的用处非常多,包括给CSS自动加前缀、使用下一代CSS语法等。
PostCSS和CSS的关系就像Babel和JavaScript的关系,它们解除了语法上的禁锢,通过插件机制来扩展语言本身,用工程化手段给语言带来了更多的可能性。
PostCSS和SCSS的关系就像Babel和TypeScript的关系,PostCSS更加灵活、可扩张性强,而SCSS内置了大量功能而不能扩展。
让我们来看一些例子。给CSS自动加前缀,增加各浏览器的兼容性:
/*输入*/
h1 {
display: flex;
}
/*输出*/
h1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
}
使用下一代CSS语法:
/*输入*/
:root {
--red: #d33;
}
h1 {
color: var(--red);
}
/*输出*/
h1 {
color: #d33;
}
PostCSS全部采用JavaScript编写,运行在Node.js之上,即提供了给JavaScript代码调用的模块,也提供了可执行的文件。在PostCSS启动时,会从目录下的postcss.config.js
文件中读取所需配置,所以需要新建该文件,文件内容大致如下:
module.exports = {
plugins: [
// 需要使用的插件列表
require('postcss-cssnext')
]
}
其中的postcss-cssnext
插件可以让你使用下一代CSS语法编写代码,再通过PostCSS转换成目前的浏览器可识别的CSS,并且该插件还包含给CSS自动加前缀的功能。
目前Chrome等现代浏览器已经能完全支持cssnext
中的所有语法,也就是说按照cssnext
语法写的CSS在不经过转换的情况下也能在浏览器中直接运行。
接入Webpack
虽然使用PostCSS后文件后缀还是.css
但这些文件必须先交给postcss-loader
处理一遍后再交给css-loader
。
接入PostCSS相关的Webpack配置如下:
module.exports = {
module: {
rules: [
{
// 使用 PostCSS 处理 CSS 文件
test: /\.css/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
]
},
};
接入PostCSS给项目带来了新的依赖需要安装,如下:
# 安装 Webpack Loader 依赖
npm i -D postcss-loader css-loader style-loader
# 根据你使用的特性安装对应的 PostCSS 插件依赖
npm i -D postcss-cssnext