webpack实战

webpack实战

查看所有文档页面: 全栈开发,获取更多信息。

快马加鞭,加班加点,终于把这个文档整理出来了,顺便深入地学习一番,巩固知识,就是太累人,影响睡眠时间和质量。极客就是想要把事情做到极致,开始了就必须到达终点。

原文链接:webpack实战,原文广告模态框遮挡,阅读体验不好,所以整理成本文,方便查找。

本章教你如何用 Webpack 去解决实际项目中常见的场景。

按照不同场景划分成以下几类:

  • 使用新语言来开发项目:

    • 使用 ES6 语言
    • 使用 TypeScript 语言
    • 使用 Flow 检查器
    • 使用 SCSS 语言
    • 使用 PostCSS
  • 使用新框架来开发项目:

    • 使用 React 框架
    • 使用 Vue 框架
    • 使用 Angular2 框架
  • 用 Webpack 构建单页应用:

    • 为单页应用生成 HTML
    • 管理多个单页应用
  • 用 Webpack 构建不同运行环境的项目:

    • 构建同构应用
    • 构建 Electron 应用
    • 构建 Npm 模块
    • 构建离线应用
  • Webpack 结合其它工具搭配使用,各取所长:

    • 搭配 Npm Script
    • 检查代码
    • 通过 Node.js API 启动 Webpack
    • 使用 Webpack Dev Middleware
  • 用 Webpack 加载特殊类型的资源:

    • 加载图片
    • 加载SVG
    • 加载 Source Map

使用 TypeScript 语言

由于本文不推荐使用TypeScript,ES6就足够完成大部分任务。原文链接:使用 TypeScript 语言

使用 Angular2 框架

Angular2不在我的技术栈范围,所以这一章不加入,有兴趣的查看原文:使用 Angular2 框架

使用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 标准里的特性,由于之前每年都有新特性被加入到标准里;

    • env 包含当前所有 ECMAScript 标准里的最新特性。
  2. 被社区提出来的但还未被写入 ECMAScript 标准里特性,这其中又分为以下四种:

    • stage0 只是一个美好激进的想法,有 Babel 插件实现了对这些特性的支持,但是不确定是否会被定为标准;
    • stage1 值得被纳入标准的特性;
    • stage2 该特性规范已经被起草,将会被纳入标准里;
    • stage3 该特性规范已经定稿,各大浏览器厂商和 Node.js 社区开始着手实现;
    • stage4 在接下来的一年将会加入到标准里去。
  3. 为了支持一些特定应用场景下的语法,和 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

使用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 代码。

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

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 混在一起,可以使用1-5 使用Plugin 中介绍过的 ExtractTextPlugin。

由于接入 sass-loader,项目需要安装这些新的依赖:

# 安装 Webpack Loader 依赖
npm i -D  sass-loader css-loader style-loader
# sass-loader 依赖 node-sass
npm i -D node-sass
    

使用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 检测器由高性能跨平台的 OCaml 语言编写,它的可执行文件可以通过:

npm i -D flow-bin

安装,安装完成后通过先配置 Npm Script:

"scripts": {
   "flow": "flow"
}

再通过 npm run flow 去调用 Flow 执行代码检查。

除此之外你还可以通过:

npm i -g flow-bin

把 Flow 安装到全局后,再直接通过 flow 命令去执行代码检查。

安装成功后,在项目根目录下执行 Flow 后,Flow 会遍历出所有需要检查的文件并对其进行检查,输出错误结果到控制台。

采用了 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。

  1. 安装 npm i -D babel-preset-flow 依赖到项目。
  2. 修改 .babelrc 配置文件,加入 Flow Preset:

    "presets": [
    ...[],
    "flow"
    ]

往源码里加入静态类型后重新构建项目,你会发现采用了 Flow 的源码还是能正常在浏览器中运行。

要明确构建的目的只是为了去除源码中的 Flow 静态类型语法,而代码检查和构建无关。 许多编辑器已经整合 Flow,可以实时在代码中高亮指出 Flow 检查出的问题。

使用PostCSS

PostCSS 是一个 CSS 处理工具,和 SCSS 不同的地方在于它通过插件机制可以灵活的扩展其支持的特性,而不是像 SCSS 那样语法是固定的。 PostCSS 的用处非常多,包括给 CSS 自动加前缀、使用下一代 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
    

使用React框架

React 语法特征

使用了 React 项目的代码特征有 JSX 和 Class 语法,例如:

class Button extends Component {
  render() {
    return 

Hello,Webpack

} }
在使用了 React 的项目里 JSX 和 Class 语法并不是必须的,但使用新语法写出的代码看上去更优雅。

其中 JSX 语法是无法在任何现有的 JavaScript 引擎中运行的,所以在构建过程中需要把源码转换成可以运行的代码,例如:

// 原 JSX 语法代码
return 

Hello,Webpack

// 被转换成正常的 JavaScript 代码 return React.createElement('h1', null, 'Hello,Webpack')

React 与 Babel

要在使用 Babel 的项目中接入 React 框架是很简单的,只需要加入 React 所依赖的 Presets babel-preset-react

通过以下命令:

# 安装 React 基础依赖
npm i -D react react-dom
# 安装 babel 完成语法转换所需依赖
npm i -D babel-preset-react

安装新的依赖后,再修改 .babelrc 配置文件加入 React Presets

"presets": [
    "react"
],

就完成了一切准备工作。

再修改 main.js 文件如下:

import * as React from 'react';
import { Component } from 'react';
import { render } from 'react-dom';

class Button extends Component {
  render() {
    return 

Hello,Webpack

} } render(

重新执行构建打开网页你将会发现由 React 渲染出来的 Hello,Webpack

React 与 TypeScript

TypeScript 相比于 Babel 的优点在于它原生支持 JSX 语法,你不需要重新安装新的依赖,只需修改一行配置。 但 TypeScript 的不同在于:

  • 使用了 JSX 语法的文件后缀必须是 tsx
  • 由于 React 不是采用 TypeScript 编写的,需要安装 reactreact-dom 对应的 TypeScript 接口描述模块 @types/react@types/react-dom 后才能通过编译。

修改 TypeScript 编译器配置文件 tsconfig.json 增加对 JSX 语法的支持,如下:

{
  "compilerOptions": {
    "jsx": "react" // 开启 jsx ,支持 React
  }
}

由于 main.js 文件中存在 JSX 语法,再把 main.js 文件重命名为 main.tsx,同时修改文件内容为在上面 React 与 Babel 里所采用的 React 代码。 同时为了让 Webpack 对项目里的 tstsx 原文件都采用 awesome-typescript-loader 去转换, 需要注意的是 Webpack Loader 配置的 test 选项需要匹配到 tsx 类型的文件,并且 extensions 中也要加上 .tsx,配置如下:

module.exports = {
  // TS 执行入口文件
  entry: './main',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './dist'),
  },
  resolve: {
    // 先尝试 ts,tsx 后缀的 TypeScript 源码文件 
    extensions: ['.ts', '.tsx', '.js',] 
  },
  module: {
    rules: [
      {
        // 同时匹配 ts,tsx 后缀的 TypeScript 源码文件 
        test: /\.tsx?$/,
        loader: 'awesome-typescript-loader'
      }
    ]
  },
  devtool: 'source-map',// 输出 Source Map 方便在浏览器里调试 TypeScript 代码
};

通过npm i react react-dom @types/react @types/react-dom安装新的依赖后重启构建,重新打开网页你将会发现由 React 渲染出来的 Hello,Webpack

使用Vue框架

Vue是一个渐进式的 MVVM 框架,相比于 React、Angular 它更灵活轻量。 它不会强制性地内置一些功能和语法,你可以根据自己的需要一点点地添加功能。 虽然采用 Vue 的项目能用可直接运行在浏览器环境里的代码编写,但为了方便编码大多数项目都会采用 Vue 官方的单文件组件的写法去编写项目。

Vue 的单文件组件通过一个类似 HTML 文件的 .vue 文件就能描述清楚一个组件所需的模版、样式、逻辑。

main.js 入口文件:

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  render: h => h(App)
});

入口文件创建一个 Vue 的根实例,在 ID 为 app 的 DOM 节点上渲染出上面定义的 App 组件。

接入 Webpack

目前最成熟和流行的开发 Vue 项目的方式是采用 ES6 加 Babel 转换,这和基本的采用 ES6 开发的项目很相似,差别在于要解析 .vue 格式的单文件组件。 好在 Vue 官方提供了对应的 vue-loader 可以非常方便的完成单文件组件的转换。

修改 Webpack 相关配置如下:

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

安装新引入的依赖:

# Vue 框架运行需要的库
npm i -S vue
# 构建所需的依赖
npm i -D vue-loader css-loader vue-template-compiler

在这些依赖中,它们的作用分别是:

  • vue-loader:解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理。
  • css-loader:加载由 vue-loader 提取出的 CSS 代码。
  • vue-template-compiler:把 vue-loader 提取出的 HTML 模版编译成对应的可执行的 JavaScript 代码,这和 React 中的 JSX 语法被编译成 JavaScript 代码类似。预先编译好 HTML 模版相对于在浏览器中再去编译 HTML 模版的好处在于性能更好。

使用 TypeScript 编写 Vue 应用

从 Vue 2.5.0+ 版本开始,提供了对 TypeScript 的良好支持,使用 TypeScript 编写 Vue 是一个很好的选择,因为 TypeScript 能检查出一些潜在的错误。

新增 tsconfig.json 配置文件,内容如下:

{
  "compilerOptions": {
    // 构建出 ES5 版本的 JavaScript,与 Vue 的浏览器支持保持一致
    "target": "es5",
    // 开启严格模式,这可以对 `this` 上的数据属性进行更严格的推断
    "strict": true,
    // TypeScript 编译器输出的 JavaScript 采用 es2015 模块化,使 Tree Shaking 生效
    "module": "es2015",
    "moduleResolution": "node"
  }
}

修改 App.vue 脚本部分内容如下:



注意 script 标签中的 lang="ts" 是为了指明代码的语法是 TypeScript。

修改 main.ts 执行入口文件为如下:

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: '#app',
  render: h => h(App)
});

由于 TypeScript 不认识 .vue 结尾的文件,为了让其支持 import App from './App.vue' 导入语句,还需要以下文件 vue-shims.d.ts 去定义 .vue 的类型:

// 告诉 TypeScript 编译器 .vue 文件其实是一个 Vue  
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

Webpack 配置需要修改两个地方,如下:

const path = require('path');

module.exports = {
  resolve: {
    // 增加对 TypeScript 的 .ts 和 .vue 文件的支持
    extensions: ['.ts', '.js', '.vue', '.json'],
  },
  module: {
    rules: [
      // 加载 .ts 文件
      {
        test: /\.ts$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
        options: {
          // 让 tsc 把 vue 文件当成一个 TypeScript 模块去处理,以解决 moudle not found 的问题,tsc 本身不会处理 .vue 结尾的文件
          appendTsSuffixTo: [/\.vue$/],
        }
      },
    ]
  },
};

除此之外还需要安装新引入的依赖:npm i -D ts-loader typescript

为单页应用生成HTML

引入问题

在使用 React 框架中,是用最简单的 Hello,Webpack 作为例子让大家理解, 这个例子里因为只输出了一个 bundle.js 文件,所以手写了一个 index.html 文件去引入这个 bundle.js,才能让应用在浏览器中运行起来。

在实际项目中远比这复杂,一个页面常常有很多资源要加载。接下来举一个实战中的例子,要求如下:

  1. 项目采用 ES6 语言加 React 框架。
  2. 给页面加入 Google Analytics,这部分代码需要内嵌进 HEAD 标签里去。
  3. 给页面加入 Disqus 用户评论,这部分代码需要异步加载以提升首屏加载速度。
  4. 压缩和分离 JavaScript 和 CSS 代码,提升加载速度。

在开始前先来看看该应用最终发布到线上的代码。

可以看到部分代码被内嵌进了 HTML 的 HEAD 标签中,部分文件的文件名称被打上根据文件内容算出的 Hash 值,并且加载这些文件的 URL 地址也被正常的注入到了 HTML 中。

解决方案

推荐一个用于方便地解决以上问题的 Webpack 插件 web-webpack-plugin。 该插件已经被社区上许多人使用和验证,解决了大家的痛点获得了很多好评,下面具体介绍如何用它来解决上面的问题。

首先,修改 Webpack 配置。

以上配置中,大多数都是按照前面已经讲过的内容增加的配置,例如:

  • 增加对 CSS 文件的支持,提取出 Chunk 中的 CSS 代码到单独的文件中,压缩 CSS 文件;
  • 定义 NODE_ENV 环境变量为 production,以去除源码中只有开发时才需要的部分;
  • 给输出的文件名称加上 Hash 值;
  • 压缩输出的 JavaScript 代码。

但最核心的部分在于 plugins 里的:

new WebPlugin({
  template: './template.html', // HTML 模版文件所在的文件路径
  filename: 'index.html' // 输出的 HTML 的文件名称
})

其中 template: './template.html' 所指的模版文件 template.html 的内容是:


  
  
  
  
  
  
  


该文件描述了哪些资源需要被以何种方式加入到输出的 HTML 文件中。

为例,按照正常引入 CSS 文件一样的语法来引入 Webpack 生产的代码。href 属性中的 app?_inline 可以分为两部分,前面的 app 表示 CSS 代码来自名叫 app 的 Chunk 中,后面的 _inline 表示这些代码需要被内嵌到这个标签所在的位置。

同样的 表示 JavaScript 代码来自相对于当前模版文件 template.html 的本地文件 ./google_analytics.js, 而且文件中的 JavaScript 代码也需要被内嵌到这个标签所在的位置。

也就是说资源链接 URL 字符串里问号前面的部分表示资源内容来自哪里,后面的 querystring 表示这些资源注入的方式。

除了 _inline 表示内嵌外,还支持以下属性:

  • _dist 只有在生产环境下才引入该资源;
  • _dev 只有在开发环境下才引入该资源;
  • _ie 只有IE浏览器才需要引入的资源,通过 [if IE]>resource 注释实现。

这些属性之间可以搭配使用,互不冲突。例如 app?_inline&_dist 表示只在生产环境下才引入该资源,并且需要内嵌到 HTML 里去。

WebPlugin 插件还支持一些其它更高级的用法,详情可以访问该项目主页阅读文档。

管理多个单页应用

引入问题

在开始前先来看看该应用最终发布到线上的代码。














构建出的目录结构为:

dist
├── common_029086ff.js
├── common_7cc98ad0.css
├── index.html
├── index_04c08fbf.css
├── index_b3d3761c.js
├── login.html
├── login_0a3feca9.js
└── login_e31e214b.css

如果按照上节的思路,可能需要为每个单页应用配置一段如下代码:

new WebPlugin({
  template: './template.html', // HTML 模版文件所在的文件路径
  filename: 'login.html' // 输出的 HTML 的文件名称
})

并且把页面对应的入口加入到 enrty 配置项中,就像这样:

entry: {
  index: './pages/index/index.js',// 页面 index.html 的入口文件
  login: './pages/login/index.js',// 页面 login.html 的入口文件
}

当有新页面加入时就需要修改 Webpack 配置文件,新插入一段以上代码,这会导致构建代码难以维护而且易错。

解决方案

项目源码目录结构如下:

├── pages
│   ├── index
│   │   ├── index.css // 该页面单独需要的 CSS 样式
│   │   └── index.js // 该页面的入口文件
│   └── login
│       ├── index.css
│       └── index.js
├── common.css // 所有页面都需要的公共 CSS 样式
├── google_analytics.js
├── template.html
└── webpack.config.js

从目录结构中可以看成出下几点要求:

  • 所有单页应用的代码都需要放到一个目录下,例如都放在 pages 目录下;
  • 一个单页应用一个单独的文件夹,例如最后生成的 index.html 相关的代码都在 index 目录下,login.html 同理;
  • 每个单页应用的目录下都有一个 index.js 文件作为入口执行文件。
虽然 AutoWebPlugin 强制性的规定了项目部分的目录结构,但从实战经验来看这是一种优雅的目录规范,合理的拆分了代码,又能让新人快速的看懂项目结构,也方便日后的维护。

Webpack 配置文件修改如下:

See the Pen webpack管理多个单页应用 by whjin (@whjin) on CodePen.


由于这个模版文件被当作项目中所有单页应用的模版,就不能再像上一节中直接写 Chunk 的名称去引入资源,因为需要被注入到当前页面的 Chunk 名称是不定的,每个单页应用都会有自己的名称。 的作用在于保证该页面所依赖的资源都会被注入到生成的 HTML 模版里去。

web-webpack-plugin 能分析出每个页面依赖哪些资源,例如对于 login.html 来说,插件可以确定该页面依赖以下资源:

  • 所有页面都依赖的公共 CSS 代码 common.css
  • 所有页面都依赖的公共 JavaScrip 代码 common.js
  • 只有这个页面依赖的 CSS 代码 login.css
  • 只有这个页面依赖的 JavaScrip 代码 login.css

由于模版文件 template.html 里没有指出引入这些依赖资源的 HTML 语句,插件会自动将没有手动导入但页面依赖的资源按照不同类型注入到 所在的位置。

  • CSS 类型的文件注入到 所在的位置,如果 不存在就注入到 HTML HEAD 标签的最后;
  • JavaScrip 类型的文件注入到 所在的位置,如果 不存在就注入到 HTML BODY 标签的最后。

如果后续有新的页面需要开发,只需要在 pages 目录下新建一个目录,目录名称取为输出 HTML 文件的名称,目录下放这个页面相关的代码即可,无需改动构建代码。

由于 AutoWebPlugin 是间接的通过上一节提到的 WebPlugin 实现的,WebPlugin 支持的功能 AutoWebPlugin 都支持。

构建同构应用

同构应用是指写一份代码但可同时在浏览器和服务器中运行的应用。

认识同构应用

现在大多数单页应用的视图都是通过 JavaScript 代码在浏览器端渲染出来的,但在浏览器端渲染的坏处有:

  • 搜索引擎无法收录你的网页,因为展示出的数据都是在浏览器端异步渲染出来的,大部分爬虫无法获取到这些数据。
  • 对于复杂的单页应用,渲染过程计算量大,对低端移动设备来说可能会有性能问题,用户能明显感知到首屏的渲染延迟。

为了解决以上问题,有人提出能否将原本只运行在浏览器中的 JavaScript 渲染代码也在服务器端运行,在服务器端渲染出带内容的 HTML 后再返回。 这样就能让搜索引擎爬虫直接抓取到带数据的 HTML,同时也能降低首屏渲染时间。 由于 Node.js 的流行和成熟,以及虚拟 DOM 提出与实现,使这个假设成为可能。

实际上现在主流的前端框架都支持同构,包括 React、Vue2、Angular2,其中最先支持也是最成熟的同构方案是 React。 由于 React 使用者更多,它们之间又很相似,本节只介绍如何用 Webpack 构建 React 同构应用。

同构应用运行原理的核心在于虚拟 DOM,虚拟 DOM 的意思是不直接操作 DOM 而是通过 JavaScript Object 去描述原本的 DOM 结构。 在需要更新 DOM 时不直接操作 DOM 树,而是通过更新 JavaScript Object 后再映射成 DOM 操作。

虚拟 DOM 的优点在于:

  • 因为操作 DOM 树是高耗时的操作,尽量减少 DOM 树操作能优化网页性能。而 DOM Diff 算法能找出2个不同 Object 的最小差异,得出最小 DOM 操作;
  • 虚拟 DOM 的在渲染的时候不仅仅可以通过操作 DOM 树来表示出结果,也能有其它的表示方式,例如把虚拟 DOM 渲染成字符串(服务器端渲染),或者渲染成手机 App 原生的 UI 组件( React Native)。

以 React 为例,核心模块 react 负责管理 React 组件的生命周期,而具体的渲染工作可以交给 react-dom 模块来负责。

react-dom 在渲染虚拟 DOM 树时有2中方式可选:

  • 通过 render() 函数去操作浏览器 DOM 树来展示出结果。
  • 通过 renderToString() 计算出表示虚拟 DOM 的 HTML 形式的字符串。

构建同构应用的最终目的是从一份项目源码中构建出2份 JavaScript 代码,一份用于在浏览器端运行,一份用于在 Node.js 环境中运行渲染出 HTML。 其中用于在 Node.js 环境中运行的 JavaScript 代码需要注意以下几点:

  • 不能包含浏览器环境提供的 API,例如使用 document 进行 DOM 操作,因为 Node.js 不支持这些 API;
  • 不能包含 CSS 代码,因为服务端渲染的目的是渲染出 HTML 内容,渲染出 CSS 代码会增加额外的计算量,影响服务端渲染性能;
  • 不能像用于浏览器环境的输出代码那样把 node_modules 里的第三方模块和 Node.js 原生模块(例如 fs 模块)打包进去,而是需要通过 CommonJS 规范去引入这些模块。
  • 需要通过 CommonJS 规范导出一个渲染函数,以用于在 HTTP 服务器中去执行这个渲染函数,渲染出 HTML 内容返回。

解决方案

用于构建浏览器环境代码的 webpack.config.js 配置文件保留不变,新建一个专门用于构建服务端渲染代码的配置文件 webpack_server.config.js,内容如下:

const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  // JS 执行入口文件
  entry: './main_server.js',
  // 为了不把 Node.js 内置的模块打包进输出文件中,例如 fs net 模块等
  target: 'node',
  // 为了不把 node_modules 目录下的第三方模块打包进输出文件中
  externals: [nodeExternals()],
  output: {
    // 为了以 CommonJS2 规范导出渲染函数,以给采用 Node.js 编写的 HTTP 服务调用
    libraryTarget: 'commonjs2',
    // 把最终可在 Node.js 中运行的代码输出到一个 bundle_server.js 文件
    filename: 'bundle_server.js',
    // 输出文件都放到 dist 目录下
    path: path.resolve(__dirname, './dist'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        // CSS 代码不能被打包进用于服务端的代码中去,忽略掉 CSS 文件
        test: /\.css/,
        use: ['ignore-loader'],
      },
    ]
  },
  devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码
};

以上代码有几个关键的地方,分别是:

  • target: 'node' 由于输出代码的运行环境是 Node.js,源码中依赖的 Node.js 原生模块没必要打包进去;
  • externals: [nodeExternals()] webpack-node-externals 的目的是为了防止 node_modules 目录下的第三方模块被打包进去,因为 Node.js 默认会去 node_modules 目录下寻找和使用第三方模块;
  • {test: /\.css/, use: ['ignore-loader']} 忽略掉依赖的 CSS 文件,CSS 会影响服务端渲染性能,又是做服务端渲不重要的部分;
  • libraryTarget: 'commonjs2' 以 CommonJS2 规范导出渲染函数,以供给采用 Node.js 编写的 HTTP 服务器代码调用。

为了最大限度的复用代码,需要调整下目录结构:

把页面的根组件放到一个单独的文件 AppComponent.js,该文件只能包含根组件的代码,不能包含渲染入口的代码,而且需要导出根组件以供给渲染入口调用,AppComponent.js 内容如下:

import React, { Component } from 'react';
import './main.css';

export class AppComponent extends Component {
  render() {
    return 

Hello,Webpack

} }

分别为不同环境的渲染入口写两份不同的文件,分别是用于浏览器端渲染 DOM 的 main_browser.js 文件,和用于服务端渲染 HTML 字符串的 main_server.js 文件。

main_browser.js 文件内容如下:

import React from 'react';
import { render } from 'react-dom';
import { AppComponent } from './AppComponent';

// 把根组件渲染到 DOM 树上
render(, window.document.getElementById('app'));

main_server.js 文件内容如下:

为了能把渲染的完整 HTML 文件通过 HTTP 服务返回给请求端,还需要通过用 Node.js 编写一个 HTTP 服务器。 由于本节不专注于将 HTTP 服务器的实现,就采用了 ExpressJS 来实现,http_server.js 文件内容如下:

const express = require('express');
const { render } = require('./dist/bundle_server');
const app = express();

// 调用构建出的 bundle_server.js 中暴露出的渲染函数,再拼接下 HTML 模版,形成完整的 HTML 文件
app.get('/', function (req, res) {
  res.send(`


  


${render()}
`); }); // 其它请求路径返回对应的本地文件 app.use(express.static('.')); app.listen(3000, function () { console.log('app listening on port 3000!') });

再安装新引入的第三方依赖:

# 安装 Webpack 构建依赖
npm i -D css-loader style-loader ignore-loader webpack-node-externals
# 安装 HTTP 服务器依赖
npm i -S express

以上所有准备工作已经完成,接下来执行构建,编译出目标文件:

  • 执行命令 webpack --config webpack_server.config.js 构建出用于服务端渲染的 ./dist/bundle_server.js 文件。
  • 执行命令 webpack 构建出用于浏览器环境运行的 ./dist/bundle_browser.js 文件,默认的配置文件为 webpack.config.js

构建执行完成后,执行 node ./http_server.js 启动 HTTP 服务器后,再用浏览器去访问 http://localhost:3000 就能看到 Hello,Webpack 了。 但是为了验证服务端渲染的结果,你需要打开浏览器的开发工具中的网络抓包一栏,再重新刷新浏览器后,就能抓到请求 HTML 的包了,抓包效果图如下:

webpack实战_第1张图片

可以看到服务器返回的是渲染出内容后的 HTML 而不是 HTML 模版,这说明同构应用的改造完成。

本实例提供 项目完整代码

构建Electron应用

Electron 是 Node.js 和 Chromium 浏览器的结合体,用 Chromium 浏览器显示出的 Web 页面作为应用的 GUI,通过 Node.js 去和操作系统交互。 当你在 Electron 应用中的一个窗口操作时,实际上是在操作一个网页。当你的操作需要通过操作系统去完成时,网页会通过 Node.js 去和操作系统交互。

采用这种方式开发桌面端应用的优点有:

  • 降低开发门槛,只需掌握网页开发技术和 Node.js 即可,大量的 Web 开发技术和现成库可以复用于 Electron;
  • 由于 Chromium 浏览器和 Node.js 都是跨平台的,Electron 能做到写一份代码在不同的操作系统运行。

在运行 Electron 应用时,会从启动一个主进程开始。主进程的启动是通过 Node.js 去执行一个入口 JavaScript 文件实现的,这个入口文件 main.js 内容如下:

See the Pen Electron-main.js by whjin (@whjin) on CodePen.


你可能感兴趣的:(webpack实战)