Webpack实战——使用新语言

使用ES6语言

虽然目前部分浏览器和Node.js已经支持ES6,但由于它们对ES6所有的标准支持不全,这导致在开发中不敢全面地使用ES6。
通常我们需要把采用ES6编写的代码转换成目前已经支持良好的ES5代码,这包含2件事:

  1. 把新的ES6语法用ES5实现,例如ES6的class语法用ES5的prototype实现。
  2. 给新的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-runtimebabel-runtime需要配套使用,使用了 babel-plugin-transform-runtime后一定需要babel-runtime

Presets

presets 属性告诉 Babel 要转换的源码使用了哪些新的语法特性,一个 Presets 对一组新语法特性提供支持,多个 Presets 可以叠加。 Presets 其实是一组 Plugins 的集合,每一个 Plugin 完成一个新语法的转换工作。Presets 是按照 ECMAScript 草案来组织的,通常可以分为以下三大类:

  1. 已经被写入 ECMAScript 标准里的特性,由于之前每年都有新特性被加入到标准里,所以又可细分为:

    • es2015 包含在2015里加入的新特性;
    • es2016 包含在2016里加入的新特性;
    • es2017 包含在2017里加入的新特性;
    • env 包含当前所有 ECMAScript 标准里的最新特性。

    它们之间的关系如图:

  1. 被社区提出来的但还未被写入ECMAScript标准里特性,这其中又分为以下四种:
    • stage0 只是一个美好激进的想法,有Babel插件实现了对这些特性的支持,但是不确定是否会被定为标准;
    • stage1 值得被纳入标准的特性;
    • stage2 该特性规范已经被起草,将会被纳入标准里;
    • stage3 该特性规范已经定稿,各大浏览器厂商和 Node.js 社区开始着手实现;
    • stage4 在接下来的一年将会加入到标准里去。

它们之间的关系如图:

  1. 为了支持一些特定应用场景下的语法,和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.jshello.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个问题:

  1. 通过Loader把TypeScript转换成JavaScript。
  2. 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) {}

有两种方式可以做到这点:

  1. flow-remove-types 可单独使用,速度快。
  2. babel-preset-flow 与 Babel 集成。

集成Webpack

由于使用了Flow项目一般都会使用ES6语法,所以把Flow集成到使用Webpack构建的项目里最方便的方法是借助Babel。加入Flow代码检查,如下:

  1. 安装npm i -D babel-preset-flow依赖到项目。
  2. 修改.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去处理。具体处理流程如下:

  1. 通过sass-loader把SCSS源码转换为CSS代码,再把CSS代码交给css-loader去处理。
  2. css-loader会找出CSS代码中的@importurl()这样的导入语句,告诉Webpack依赖这些资源。同时还支持CSS Modules、压缩CSS等功能。处理完后再把结果交给style-loader去处理。
  3. 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容