>  기사  >  웹 프론트엔드  >  웹팩 실제 사례에 대한 자세한 설명

웹팩 실제 사례에 대한 자세한 설명

php中世界最好的语言
php中世界最好的语言원래의
2018-05-25 11:26:152015검색

이번에는 webpack의 실제 사례에 대해 자세히 설명하겠습니다. webpack의 주의 사항은 무엇입니까? 실제 사례를 살펴 보겠습니다.

Webpack 작동 중

모든 문서 페이지 보기: 전체 스택 개발에서 자세한 내용을 확인하세요.

저도 이 문서를 정리하기 위해 정말 열심히 일하고 야근을 했습니다. 저도 깊이 공부하고 지식을 쌓았지만 너무 피곤해서 수면 시간과 질에 영향을 미쳤습니다. 괴짜들은 일을 극단적으로 하고 싶어합니다. 일단 시작하면 끝까지 도달해야 합니다.

원본 링크 : 웹팩 실제 전투, 원본 광고 모달박스가 막혀 있고, 읽기 경험도 좋지 않아서 검색하기 쉽도록 이 글로 정리했습니다.

이 장에서는 Webpack을 사용하여 실제 프로젝트에서 일반적인 시나리오를 해결하는 방법을 설명합니다.

다양한 시나리오에 따라 다음 범주로 구분:

  • 새로운 언어를 사용하여 프로젝트 개발:

    • ES6 언어 사용

    • TypeScript 언어 사용

    • Flow Inspector 사용

    • SCSS 언어 사용

    • PostCSS 사용

  • 새로운 프레임워크를 사용하여 프로젝트 개발:

    • React 프레임워크 사용

    • Vue 프레임워크 사용

    • Angular2 프레임워크 사용

  • 빌드 Webpack 단일 페이지 애플리케이션 사용:

    • 단일 페이지 애플리케이션용 HTML 생성

    • 여러 단일 페이지 애플리케이션 관리

  • Webpack을 사용하여 다양한 실행 환경에서 프로젝트 빌드:

    • 빌드 동형 애플리케이션

    • Electron 애플리케이션 구축

    • Npm 모듈 구축

    • 오프라인 애플리케이션 구축

  • Webpack 다른 도구와 함께 사용하여 각각의 장점을 최대한 활용하세요. 와 함께 방영됨 Npm 스크립트

    • 코드 확인

    • Node.js API를 통해 Webpack 시작

    • Webpack Dev Middleware 사용

    • Webpack을 사용하여 특수 유형의 리소스 로드

  • 로드 이미지

    • SVG 로드

    • 소스 맵 로드

    • TypeScript 언어 사용

    • 이 기사에서는 TypeScript를 권장하지 않으므로 ES6이면 대부분의 작업을 완료하기에 충분합니다. 원본 링크: TypeScript 언어 사용
Using Angular2 프레임워크

Angular2는 내 기술 스택 범위에 포함되지 않으므로 이 장은 포함되지 않습니다. 관심이 있다면 원본 텍스트: Using Angular2 Framework

Using ES6를 확인하세요. 언어

ES6 사용 일반적으로 작성된 코드를 잘 지원되는 ES5 코드로 변환합니다. 여기에는 두 가지가 포함됩니다.

ES5를 사용하여 새로운 ES6 구문을 구현합니다. 예를 들어 ES6의 class 구문은 ES5의 클래스를 사용합니다. 프로토타입 구현.

  1. 새 API에 폴리필을 주입합니다. 예를 들어 새로운 fetch API를 사용할 때 해당 폴리필을 주입하면 저사양 브라우저가 정상적으로 실행될 수 있습니다.

    class 语法用 ES5 的 prototype 实现。

  2. 给新的 API 注入 polyfill ,例如使用新的 fetch API 时注入对应的 polyfill 后才能让低端浏览器正常运行。

Babel

Babel 可以方便的完成以上2件事。

Babel 是一个 JavaScript 编译器,能将 ES6 代码转为 ES5 代码,让你使用最新的语言特性而不用担心兼容性问题,并且可以通过插件机制根据需求灵活的扩展。

在 Babel 执行编译的过程中,会从项目根目录下的 .babelrc 文件读取配置.babelrc

Babel🎜🎜Babel은 위의 두 가지를 쉽게 수행할 수 있습니다. 🎜🎜Babel은 ES6 코드를 ES5 코드로 변환할 수 있는 JavaScript 컴파일러로, 호환성 문제에 대한 걱정 없이 최신 언어 기능을 사용할 수 있으며 플러그인 메커니즘을 통해 필요에 따라 유연하게 확장할 수 있습니다. 🎜🎜Babel의 컴파일 과정에서 .babelrc 파일 구성 읽기🎜. .babelrc는 다음 내용을 포함하는 JSON 형식 파일입니다. 🎜
{
  "plugins": [
    [
      "transform-runtime",
      {
        "polyfill": false
      }
    ]
   ],
  "presets": [
    [
      "es2015",
      {
        "modules": false
      }
    ],
    "stage-2",
    "react"
  ]
}

Plugins

plugins 속성은 사용할 플러그인을 Babel에 알려주고, 플러그인은 코드 변환 방법을 제어할 수 있습니다. 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(&#39;babel-runtime/helpers/_extent&#39;);

这样能减小 Babel 编译出来的代码的文件大小。

同时需要注意的是由于 babel-plugin-transform-runtime 注入了 require(&#39;babel-runtime/helpers/_extent&#39;) 语句到编译后的代码里,需要安装 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: [&#39;babel-loader&#39;],
      },
    ]
  },
  // 输出 source-map 方便直接调试 ES6 源码
  devtool: &#39;source-map&#39;
};

配置命中了项目目录下所有的 JavaScript 文件,通过 babel-loader

위의 구성 파일transform-runtime에 해당합니다. 플러그인의 전체 이름은 babel-plugin-transform-runtime입니다. 즉, Babel을 정상적으로 실행하려면 babel-plugin-이 앞에 추가되어야 합니다. 먼저 설치하세요:

# Webpack 接入 Babel 必须依赖的模块
npm i -D babel-core babel-loader 
# 根据你的需求选择不同的 Plugins 或 Presets
npm i -D babel-preset-env

babel-plugin-transform-runtime은 Babel에서 공식적으로 제공하는 플러그인으로, 중복되는 코드를 줄이는 데 사용됩니다.

Babel은 일반적으로 ES6 코드를 ES5 코드로 변환할 때 새로운 구문을 구현하기 위해 ES5로 작성된 몇 가지 보조 함수가 필요합니다. 예를 들어 classextent 구문을 변환하면 변환된 ES5 코드에 삽입됩니다. _extent 도우미 함수는 상속을 구현하는 데 사용됩니다.

$blue: #1875e7; 
p {
  color: $blue;
}

이렇게 하면 클래스 범위 구문을 사용하는 각 파일에 중복된 _extent</code가 삽입됩니다. > 보조 함수 코드인 <code>babel-plugin-transform-runtime은 보조 함수 내용을 파일에 삽입하지 않지만 import 문을 삽입합니다. 🎜
npm i -g node-sass
🎜이렇게 하면 Babel 파일로 컴파일된 코드가 줄어들 수 있습니다. 크기. 🎜🎜동시에 babel-plugin-transform-runtimerequire('babel-runtime/helpers/_extent') 문을 컴파일된 코드의 경우, 코드가 정상적으로 실행되기 전에 프로젝트에 종속되도록 babel-runtime을 설치해야 합니다. 즉, babel-plugin-transform-runtime을 사용한 후 babel-runtime을 함께 사용해야 합니다. >인 경우 babel-runtime이 필요합니다. 🎜🎜Presets🎜🎜 presets 속성은 변환할 소스 코드에서 어떤 새로운 구문 기능이 사용되는지 Babel에 알려줍니다. 하나의 Presets는 새로운 구문 기능 세트에 대한 지원을 제공합니다. 여러 사전 설정을 쌓을 수 있습니다. 🎜🎜Presets는 실제로 플러그인 모음입니다. 각 플러그인은 새로운 구문 변환을 완료합니다. 사전 설정은 ECMAScript 초안에 따라 구성되며 일반적으로 다음 세 가지 범주로 나눌 수 있습니다. 🎜
  1. 🎜 새로운 기능으로 인해 ECMAScript 표준에 작성된 기능 기능이 표준에 추가되었습니다. 🎜
    • 🎜env에는 현재 모든 ECMAScript 표준의 최신 기능이 포함되어 있습니다. 🎜
  2. 🎜 커뮤니티에서 제안했지만 아직 ECMAScript 표준으로 작성되지 않은 기능은 다음 네 가지 유형으로 나뉩니다. 🎜
    • 🎜stage0는 아름답고 파격적인 아이디어입니다. 이러한 기능을 지원하는 Babel 플러그인이 있지만 표준으로 설정될지는 확실하지 않습니다. li>
    • 🎜stage1 표준에 포함될 가치가 있는 기능 🎜
    • 🎜stage2 이 기능 사양은 초안이 작성되었으며 다음에 포함될 예정입니다. 🎜
    • 🎜stage3 이 기능 사양이 확정되었으며 주요 브라우저 제조업체와 Node.js 커뮤니티가 이를 구현하기 시작했습니다. 🎜
    • 🎜stage4 는 내년에 표준에 추가될 예정입니다. 🎜
  3. 🎜일부 특정 애플리케이션 시나리오에서 구문을 지원하기 위해 ECMAScript 표준과 관련이 없습니다. 예를 들어 babel-preset-react React 개발 문법에서 JSX를 지원하는 것입니다. 🎜
🎜실제 응용 프로그램에서는 프로젝트 소스 코드에 사용된 구문에 따라 해당 플러그인 또는 사전 설정을 설치해야 합니다. 🎜🎜Babel에 연결🎜🎜Babel이 하는 일은 코드를 변환하는 것이므로 Loader를 통해 Babel에 연결해야 합니다. Webpack 구성은 다음과 같습니다. 🎜
# 把 main.scss 源文件编译成 main.css
node-sass main.scss main.css
🎜구성은 babel을 통해 프로젝트 디렉터리의 모든 JavaScript 파일에 연결됩니다. loader는 Babel을 호출하여 변환 작업을 완료합니다. 빌드를 다시 실행하기 전에 새로 도입된 종속성을 설치해야 합니다. 🎜
module.exports = {
  module: {
    rules: [
      {
        // 增加对 SCSS 文件的支持
        test: /\.scss/,
        // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader
        use: [&#39;style-loader&#39;, &#39;css-loader&#39;, &#39;sass-loader&#39;],
      },
    ]
  },
};
🎜SCSS 언어 사용🎜🎜SCSS를 사용하면 CSS를 보다 유연한 방식으로 작성할 수 있습니다. CSS 전처리기입니다. 구문은 CSS와 유사하지만 코드는 다음과 유사합니다. 🎜
# 安装 Webpack Loader 依赖
npm i -D  sass-loader css-loader style-loader
# sass-loader 依赖 node-sass
npm i -D node-sass
🎜SCSS는 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: [&#39;style-loader&#39;, &#39;css-loader&#39;, &#39;sass-loader&#39;],
      },
    ]
  },
};

以上配置通过正则 /\.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(&#39;2&#39;); // Error: square1 需要传入 number 作为参数
// 类型推断检查
function square2(n) {
  return n * n; // Error: 传入的 string 类型不能做乘法运算
}
square2(&#39;2&#39;);
需要注意的时代码中的第一行 // @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(&#39;postcss-cssnext&#39;)
  ]
}

其中的 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: [&#39;style-loader&#39;, &#39;css-loader&#39;, &#39;postcss-loader&#39;],
      },
    ]
  },
};

接入 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 <h1>Hello,Webpack</h1>
  }
}
在使用了 React 的项目里 JSX 和 Class 语法并不是必须的,但使用新语法写出的代码看上去更优雅。

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

// 原 JSX 语法代码
return <h1>Hello,Webpack</h1>
// 被转换成正常的 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 <h1>Hello,Webpack</h1>
  }
}
render(<Button/>, window.document.getElementById('app'));

重新执行构建打开网页你将会发现由 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">
  import Vue from "vue";
  // 通过 Vue.extend 启用 TypeScript 类型推断
  export default Vue.extend({
    data() {
      return {
        msg: 'Hello,Webpack',
      }
    },
  });
</script>

注意 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 的内容是:

<head>
  <meta charset="UTF-8">
  <!--注入 Chunk app 中的 CSS-->
  <link rel="stylesheet" href="app?_inline">
  <!--注入 google_analytics 中的 JavaScript 代码-->
  <script src="./google_analytics.js?_inline"></script>
  <!--异步加载 Disqus 评论-->
  <script src="https://pe-into-webpack.disqus.com/embed.js" async></script>
</head>
<body>
<p id="app"></p>
<!--导入 Chunk app 中的 JS-->
<script src="app"></script>
<!--Disqus 评论容器-->
<p id="disqus_thread"></p>
</body>

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

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

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

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

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

  • _dist 只有在生产环境下才引入该资源;

  • _dev 只有在开发环境下才引入该资源;

  • _ie 只有IE浏览器才需要引入的资源,通过 [if IE]>resource<![endif] 注释实现。

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

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

管理多个单页应用

引入问题

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

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


AutoWebPlugin 会找出 pages 目录下的2个文件夹 indexlogin,把这两个文件夹看成两个单页应用。 并且分别为每个单页应用生成一个 Chunk 配置和 WebPlugin 配置。 每个单页应用的 Chunk 名称就等于文件夹的名称,也就是说 autoWebPlugin.entry() 方法返回的内容其实是:

{
  "index":["./pages/index/index.js","./common.css"],
  "login":["./pages/login/index.js","./common.css"]
}

但这些事情 AutoWebPlugin 都会自动为你完成,你不用操心,明白大致原理即可。

template.html 模版文件如下:

<html>
<head>
  <meta charset="UTF-8">
  <!--在这注入该页面所依赖但没有手动导入的 CSS-->
  <!--STYLE-->
  <!--注入 google_analytics 中的 JS 代码-->
  <script src="./google_analytics.js?_inline"></script>
  <!--异步加载 Disqus 评论-->
  <script src="https://pe-into-webpack.disqus.com/embed.js" async></script>
</head>
<body>
<p id="app"></p>
<!--在这注入该页面所依赖但没有手动导入的 JavaScript-->
<!--SCRIPT-->
<!--Disqus 评论容器-->
<p id="disqus_thread"></p>
</body>
</html>

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

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

  • 所有页面都依赖的公共 CSS 代码 common.css

  • 所有页面都依赖的公共 JavaScrip 代码 common.js

  • 只有这个页面依赖的 CSS 代码 login.css

  • 只有这个页面依赖的 JavaScrip 代码 login.css

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

  • CSS 类型的文件注入到 <!--STYLE--> 所在的位置,如果 <!--STYLE--> 不存在就注入到 HTML HEAD 标签的最后;

  • JavaScrip 类型的文件注入到 <!--SCRIPT--> 所在的位置,如果 <!--SCRIPT--> 不存在就注入到 HTML BODY 标签的最后。

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

AutoWebPlugin은 앞서 언급한 WebPlugin을 통해 간접적으로 구현되기 때문에 WebPlugin에서는 AutoWebPlugin 기능을 모두 지원합니다. 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

동형 애플리케이션 구축

동형 애플리케이션은 하나의 코드를 작성하지만 브라우저와 서버에서 동시에 실행할 수 있는 애플리케이션을 의미합니다. 🎜

동형 애플리케이션 이해

🎜현재 대부분의 단일 페이지 애플리케이션의 보기는 JavaScript 코드를 통해 브라우저 측에서 렌더링됩니다. 단점은 다음과 같습니다. 🎜
  • 🎜 표시된 데이터는 브라우저 측에서 비동기적으로 렌더링되고 대부분의 크롤러는 이러한 데이터를 얻을 수 없기 때문에 검색 엔진은 웹페이지를 포함할 수 없습니다. 🎜
  • 🎜복잡한 단일 페이지 애플리케이션의 경우 렌더링 프로세스에 많은 양의 계산이 필요하므로 저사양 모바일 장치에서는 성능 문제가 발생할 수 있습니다. 사용자는 첫 번째 화면의 렌더링 지연을 명확하게 인식할 수 있습니다. 🎜
🎜위 문제를 해결하기 위해 누군가가 원래 브라우저에서만 실행되었던 JavaScript 렌더링 코드를 서버 측에서도 실행한 다음 콘텐츠가 포함된 HTML을 렌더링한 후 반환할 수 있는지 문의했습니다. 서버 측에서. 이를 통해 검색 엔진 크롤러는 데이터가 포함된 HTML을 직접 크롤링하는 동시에 첫 번째 화면 렌더링 시간을 줄일 수 있습니다. Node.js의 인기와 성숙도, 가상 DOM의 제안 및 구현으로 인해 이러한 가정이 가능합니다. 🎜🎜실제로 React, Vue2, Angular2를 포함한 모든 현재 주류 프런트엔드 프레임워크는 동형을 지원합니다. 그중 최초이자 가장 성숙한 동형 솔루션은 React입니다. React 사용자가 더 많고 매우 유사하기 때문에 이 섹션에서는 Webpack을 사용하여 React 동형 애플리케이션을 구축하는 방법만 소개합니다. 🎜🎜동형 애플리케이션의 작동 원리의 핵심은 가상 DOM에 있습니다. 가상 DOM은 DOM을 직접 운영하는 것이 아니라 JavaScript 객체를 통해 원래 DOM 구조를 설명하는 것을 의미합니다. DOM을 업데이트해야 하는 경우 DOM 트리를 직접 조작하지 않고 JavaScript 개체를 업데이트한 다음 DOM 작업에 매핑합니다. 🎜🎜가상 DOM의 장점은 다음과 같습니다. 🎜
  • 🎜DOM 트리 운영은 시간이 많이 걸리는 작업이므로 DOM 트리 작업을 최소화하면 웹 페이지 성능을 최적화할 수 있습니다. DOM Diff 알고리즘은 서로 다른 두 개체 간의 최소 차이를 찾아 최소 DOM 연산을 얻을 수 있습니다. 🎜
  • 🎜가상 DOM은 렌더링 중에 DOM 트리를 작동하여 결과를 표현할 수 있을 뿐만 아니라 가상 DOM을 문자열로 렌더링(서버 측 렌더링)하거나 모바일 앱의 기본 UI 구성 요소로 렌더링(React Native)하는 등의 다른 표현 방법입니다. 🎜
🎜React를 예로 들어보겠습니다. 핵심 모듈인 React는 라이프 사이클이며 특정 렌더링 작업은 react-dom 모듈에 맡길 수 있습니다. 🎜🎜react-dom에는 가상 DOM 트리를 렌더링할 때 2가지 옵션이 있습니다: 🎜
  • 🎜render()를 통해 결과를 표시하기 위해 브라우저 DOM 트리를 조작하는 기능입니다. 🎜
  • 🎜 renderToString()을 통해 가상 DOM을 나타내는 HTML 형식의 문자열을 계산합니다. 🎜
🎜동형 애플리케이션을 구축하는 궁극적인 목표는 하나의 프로젝트 소스 코드에서 2개의 JavaScript 코드(브라우저 측에서 실행하기 위한 코드와 Node.js 환경에서 실행하기 위한 코드)를 빌드하는 것입니다. 실행하여 렌더링 HTML. Node.js 환경에서 실행하는 데 사용되는 JavaScript 코드는 다음 사항에 주의해야 합니다. 🎜
  • 🎜 을 사용하는 등 브라우저 환경에서 제공하는 API를 포함할 수 없습니다. 문서는 Node.js가 이러한 API를 지원하지 않기 때문에 DOM 작업을 수행합니다. 🎜
  • 🎜 서버 측 렌더링의 목적은 HTML 콘텐츠를 렌더링하는 것이기 때문에 CSS 코드를 포함할 수 없습니다. 렌더링 CSS 코드가 증가합니다. 추가 계산량이 서버 측 렌더링 성능에 영향을 미칩니다. 🎜
  • 🎜 node_modules의 타사 모듈을 Node.js 기본과 결합할 수 없습니다. 브라우저 환경 모듈(예: fs 모듈)에서 사용되는 출력 코드가 패키지되어 있지만 이러한 모듈은 CommonJS 사양을 통해 도입되어야 합니다. 🎜
  • 🎜HTTP 서버에서 이 렌더링 기능을 실행하고 HTML 콘텐츠를 렌더링하여 반환하려면 CommonJS 사양을 통해 렌더링 기능을 내보내야 합니다. 🎜

솔루션

🎜webpack.config.js 브라우저 환경 코드 구축을 위한 구성 파일을 변경하지 않고 그대로 둡니다. 서버 측 렌더링 코드를 빌드하기 위해 특별히 새 구성 파일 webpack_server.config.js를 만듭니다. 내용은 다음과 같습니다. 🎜

See the Pen webpack_server.config.js by whjin (@whjin) on CodePen.


主进程启动后会一直驻留在后台运行,你眼睛所看得的和操作的窗口并不是主进程,而是由主进程新启动的窗口子进程。

应用从启动到退出有一系列生命周期事件,通过 electron.app.on() 函数去监听生命周期事件,在特定的时刻做出反应。 例如在 app.on('ready') 事件中通过 BrowserWindow 去展示应用的主窗口。

启动的窗口其实是一个网页,启动时会去加载在 loadURL 中传入的网页地址。 每个窗口都是一个单独的网页进程,窗口之间的通信需要借助主进程传递消息。

总体来说开发 Electron 应用和开发 Web 应用很相似,区别在于 Electron 的运行环境同时内置了浏览器和 Node.js 的 API,在开发网页时除了可以使用浏览器提供的 API 外,还可以使用 Node.js 提供的 API。

接入 Webpack

接下来做一个简单的 Electron 应用,要求为应用启动后显示一个主窗口,在主窗口里有一个按钮,点击这个按钮后新显示一个窗口,且使用 React 开发网页。

由于 Electron 应用中的每一个窗口对应一个网页,所以需要开发2个网页,分别是主窗口的 index.html 和新打开的窗口 login.html

需要改动的地方如下:

  • 在项目根目录下新建主进程的入口文件 main.js,内容和上面提到的一致;

  • 主窗口网页的代码如下:

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


重新执行构建后,你将会在项目目录下看到一个新目录 lib,里面放着要发布到 Npm 仓库的最终代码。

发布到 Npm

在把构建出的代码发布到 Npm 仓库前,还需要确保你的模块描述文件 package.json 是正确配置的。

由于构建出的代码的入口文件是 ./lib/index.js,需要修改 package.json 中的 main 字段如下:

{
  "main": "lib/index.js",
  "jsnext:main": "src/index.js"
}

其中 jsnext:main 字段用于指出采用 ES6 编写的模块入口文件所在的位置。

修改完毕后在项目目录下执行 npm publish 就能把构建出的代码发布到 Npm 仓库中(确保已经 npm login 过)。

如果你想让发布到 Npm 上去的代码保持和源码的目录结构一致,那么用 Webpack 将不在适合。 因为源码是一个个分割的模块化文件,而 Webpack 会把这些模块组合在一起。 虽然 Webpack 输出的文件也可以是采用 CommonJS 模块化语法的,但在有些场景下把所有模块打包成一个文件发布到 Npm 是不适合的。 例如像 Lodash 这样的工具函数库在项目中可能只用到了其中几个工具函数,如果所有工具函数打包在一个文件中,那么所有工具函数都会被打包进去,而保持模块文件的独立能做到只打包进使用到的。 还有就是像 UI 组件库这样由大量独立组件组成的库也和 Lodash 类似。
所以 Webpack 适合于构建完整不可分割的 Npm 模块。

构建离线应用

离线应用的核心是离线缓存技术,历史上曾先后出现2种离线离线缓存技术,它们分别是:

  1. AppCache 又叫 Application Cache,目前已经从 Web 标准中删除,请尽量不要使用它。

  2. Service Workers 是目前最新的离线缓存技术,是 Web Worker 的一部分。 它通过拦截网络请求实现离线缓存,比 AppCache 更加灵活。它也是构建 PWA 应用的关键技术之一。

认识 Service Workers

Service Workers 是一个在浏览器后台运行的脚本,它生命周期完全独立于网页。它无法直接访问 DOM,但可以通过 postMessage 接口发送消息来和 UI 进程通信。 拦截网络请求是 Service Workers 的一个重要功能,通过它能完成离线缓存、编辑响应、过滤响应等功能。

Service Workers 兼容性

目前 Chrome、Firefox、Opera 都已经全面支持 Service Workers,但对于移动端浏览器就不太乐观了,只有高版本的 Android 支持。 由于 Service Workers 无法通过注入 polyfill 去实现兼容,所以在你打算使用它前请先调查清楚你的网页的运行场景。

判断浏览器是否支持 Service Workers 的最简单的方法是通过以下代码:

// 如果 navigator 对象上存在 serviceWorker 对象,就表示支持
if (navigator.serviceWorker) {
  // 通过 navigator.serviceWorker 使用
}

注册 Service Workers

要给网页接入 Service Workers,需要在网页加载后注册一个描述 Service Workers 逻辑的脚本。 代码如下:

if (navigator.serviceWorker) {
  window.addEventListener('DOMContentLoaded',function() {
    // 调用 serviceWorker.register 注册,参数 /sw.js 为脚本文件所在的 URL 路径
      navigator.serviceWorker.register('/sw.js');
  });
}

一旦这个脚本文件被加载,Service Workers 的安装就开始了。这个脚本被安装到浏览器中后,就算用户关闭了当前网页,它仍会存在。 也就是说第一次打开该网页时 Service Workers 的逻辑不会生效,因为脚本还没有被加载和注册,但是以后再次打开该网页时脚本里的逻辑将会生效。

在 Chrome 中可以通过打开网址 chrome://inspect/#service-workers 来查看当前浏览器中所有注册了的 Service Workers。

使用 Service Workers 实现离线缓存

Service Workers 在注册成功后会在其生命周期中派发出一些事件,通过监听对应的事件在特点的时间节点上做一些事情。

在 Service Workers 脚本中,引入了新的关键字 self 代表当前的 Service Workers 实例。

在 Service Workers 安装成功后会派发出 install 事件,需要在这个事件中执行缓存资源的逻辑,实现代码如下:

// 当前缓存版本的唯一标识符,用当前时间代替
var cacheKey = new Date().toISOString();
// 需要被缓存的文件的 URL 列表
var cacheFileList = [
  '/index.html',
  '/app.js',
  '/app.css'
];
// 监听 install 事件
self.addEventListener('install', function (event) {
  // 等待所有资源缓存完成时,才可以进行下一步
  event.waitUntil(
    caches.open(cacheKey).then(function (cache) {
      // 要缓存的文件 URL 列表
      return cache.addAll(cacheFileList);
    })
  );
});

接下来需要监听网络请求事件去拦截请求,复用缓存,代码如下:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // 去缓存中查询对应的请求
    caches.match(event.request).then(function(response) {
        // 如果命中本地缓存,就直接返回本地的资源
        if (response) {
          return response;
        }
        // 否则就去用 fetch 下载资源
        return fetch(event.request);
      }
    )
  );
});

以上就实现了离线缓存。

更新缓存

线上的代码有时需要更新和重新发布,如果这个文件被离线缓存了,那就需要 Service Workers 脚本中有对应的逻辑去更新缓存。 这可以通过更新 Service Workers 脚本文件做到。

浏览器针对 Service Workers 有如下机制:

  1. 每次打开接入了 Service Workers 的网页时,浏览器都会去重新下载 Service Workers 脚本文件(所以要注意该脚本文件不能太大),如果发现和当前已经注册过的文件存在字节差异,就将其视为“新服务工作线程”。

  2. 新 Service Workers 线程将会启动,且将会触发其 install 事件。

  3. 当网站上当前打开的页面关闭时,旧 Service Workers 线程将会被终止,新 Service Workers 线程将会取得控制权。

  4. 新 Service Workers 线程取得控制权后,将会触发其 activate 事件。

新 Service Workers 线程中的 activate 事件就是最佳的清理旧缓存的时间点,代码如下:

// 当前缓存白名单,在新脚本的 install 事件里将使用白名单里的 key 
var cacheWhitelist = [cacheKey];
self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          // 不在白名单的缓存全部清理掉
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            // 删除缓存
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

最终完整的代码 Service Workers 脚本代码如下:

See the Pen Service Workers by whjin (@whjin) on CodePen.


以上配置有2点需要注意:

  • 由于 Service Workers 必须在 HTTPS 环境下才能拦截网络请求实现离线缓存,使用在 DevServer https 中提到的方式去实现 HTTPS 服务。

  • serviceworker-webpack-plugin 插件为了保证灵活性,允许使用者自定义 sw.js,构建输出的 sw.js 文件中会在头部注入一个变量 serviceWorkerOption.assets 到全局,里面存放着所有需要被缓存的文件的 URL 列表。

需要修改上面的 sw.js 文件中写成了静态值的 cacheFileList 为如下:

// 需要被缓存的文件的 URL 列表
var cacheFileList = global.serviceWorkerOption.assets;

以上已经完成所有文件的修改,在重新构建前,先安装新引入的依赖:

npm i -D serviceworker-webpack-plugin webpack-dev-server

安装成功后,在项目根目录下执行 webpack-dev-server 命令后,DevServer 将以 HTTPS 模式启动。

搭配Npm Script

Npm Script 是一个任务执行者。 Npm 是在安装 Node.js 时附带的包管理器,Npm Script 则是 Npm 内置的一个功能,允许在 package.json 文件里面使用 scripts 字段定义任务:

{
  "scripts": {
    "dev": "node dev.js",
    "pub": "node build.js"
  }
}

里面的 scripts 字段是一个对象,每一个属性对应一段脚本,以上定义了两个任务 devpub。 Npm Script 底层实现原理是通过调用 Shell 去运行脚本命令,例如执行 npm run pub 命令等同于执行命令 node build.js

Npm Script 还有一个重要的功能是能运行安装到项目目录里的 node_modules 里的可执行模块,例如在通过命令:

npm i -D webpack

将 Webpack 安装到项目中后,是无法直接在项目根目录下通过命令 webpack 去执行 Webpack 构建的,而是要通过命令 ./node_modules/.bin/webpack 去执行。

Npm Script 能方便的解决这个问题,只需要在 scripts 字段里定义一个任务,例如:

{
  "scripts": {
    "build": "webpack"
  }
}

Npm Script 会先去项目目录下的 node_modules 中寻找有没有可执行的 webpack 文件,如果有就使用本地的,如果没有就使用全局的。 所以现在执行 Webpack 构建只需要通过执行 npm run build 去实现。

Webpack 为什么需要 Npm Script

Webpack 只是一个打包模块化代码的工具,并没有提供任何任务管理相关的功能。 但在实际场景中通常不会是只通过执行 webpack 就能完成所有任务的,而是需要多个任务才能完成。

  1. 在开发阶段为了提高开发体验,使用 DevServer 做开发,并且需要输出 Source Map 以方便调试,同时还需要开启自动刷新功能。

  2. 为了减小发布到线上的代码尺寸,在构建出发布到线上的代码时,需要压缩输出的代码。

  3. 在构建完发布到线上的代码后,需要把构建出的代码提交给发布系统。

可以看出要求1和要求2是相互冲突的,其中任务3又依赖任务2。要满足以上三个要求,需要定义三个不同的任务。

接下来通过 Npm Script 来定义上面的3个任务:

"scripts": {
  "dev": "webpack-dev-server --open",
  "dist": "NODE_ENV=production webpack --config webpack_dist.config.js",
  "pub": "npm run dist && rsync dist"
},

含义分别是:

  • dev 代表用于开发时执行的任务,通过 DevServer 去启动构建。所以在开发项目时只需执行 npm run dev

  • dist 代表构建出用于发布到线上去的代码,输出到 dist 目录中。其中的 NODE_ENV=production 是为了在运行任务时注入环境变量。

  • pub 代表先构建出用于发布到线上去的代码,再同步 dist 目录中的文件到发布系统(如何同步文件需根据你所使用的发布系统而定)。所以在开发完后需要发布时只需执行 npm run pub

使用 Npm Script 的好处是把一连串复杂的流程简化成了一个简单的命令,需要时只需要执行对应的那个简短的命令,而不用去手动的重复整个流程。 这会大大的提高我们的效率和降低出错率。

检查代码

检查代码和 Code Review 很相似,都是去审视提交的代码可能存在的问题。 但 Code Review 一般通过人去执行,而检查代码是通过机器去执行一些自动化的检查。 自动化的检查代码成本更低,实施代价更小。

检查代码主要检查以下几项:

  • 代码风格:让项目成员强制遵守统一的代码风格,例如如何缩紧、如何写注释等,保障代码可读性,不把时间浪费在争论如何写代码更好看上;

  • 潜在问题:分析出代码在运行过程中可能出现的潜在 Bug。

目前已经有成熟的工具可以检验诸如 JavaScript、TypeScript、CSS、SCSS 等常用语言。

检查 JavaScript

目前最常用的 JavaScript 检查工具是 ESlint ,它不仅内置了大量常用的检查规则,还可以通过插件机制做到灵活扩展。

ESlint 的使用很简单,在通过:npm i -g eslint

按照到全局后,再在项目目录下执行:eslint init

来新建一个 ESlint 配置文件 .eslintrc,该文件格式为 JSON。

如果你想覆盖默认的检查规则,或者想加入新的检查规则,你需要修改该文件,例如使用以下配置:

{
    // 从 eslint:recommended 中继承所有检查规则
    "extends": "eslint:recommended",
    // 再自定义一些规则     
    "rules": {
        // 需要在每行结尾加 ;        
        "semi": ["error", "always"],
        // 需要使用 "" 包裹字符串         
        "quotes": ["error", "double"]
    }
}

写好配置文件后,再执行:

eslint yourfile.js

去检查 yourfile.js 文件,如果你的文件没有通过检查,ESlint 会输出错误原因,例如:

检查 TypeScript

TSLint 是一个和 ESlint 相似的 TypeScript 代码检查工具,区别在于 TSLint 只专注于检查 TypeScript 代码。

TSLint 和 ESlint 的使用方法很相似,首先通过:npm i -g tslint

安装到全局,再去项目根目录下执行:tslint --init

生成配置文件 tslint.json,在配置好后,再执行:tslint yourfile.ts去检查 yourfile.ts 文件。

检查 CSS

stylelint 是目前最成熟的 CSS 检查工具,内置了大量检查规则的同时也提供插件机制让用户自定义扩展。 stylelint 基于 PostCSS,能检查任何 PostCSS 能解析的代码,诸如 SCSS、Less 等。

首先通过npm i -g stylelint

安装到全局后,去项目根目录下新建 .stylelintrc 配置文件, 该配置文件格式为 JSON,其格式和 ESLint 的配置相似,例如:

{
  // 继承 stylelint-config-standard 中的所有检查规则
  "extends": "stylelint-config-standard",
  // 再自定义检查规则  
  "rules": {
    "at-rule-empty-line-before": null
  }
}

配置好后,再执行stylelint "yourfile.css"去检查 yourfile.css 文件。

结合 Webpack 检查代码

结合 ESLint

eslint-loader 可以方便的把 ESLint 整合到 Webpack 中,使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // node_modules 目录的下的代码不用检查
        exclude: /node_modules/,
        loader: 'eslint-loader',
        // 把 eslint-loader 的执行顺序放到最前面,防止其它 Loader 把处理后的代码交给 eslint-loader 去检查
        enforce: 'pre',
      },
    ],
  },
}

接入 eslint-loader 后就能在控制台中看到 ESLint 输出的错误日志了。

结合 TSLint

tslint-loader 是一个和 eslint-loader 相似的 Webpack Loader, 能方便的把 TSLint 整合到 Webpack,其使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        // node_modules 目录的下的代码不用检查
        exclude: /node_modules/,
        loader: 'tslint-loader',
        // 把 tslint-loader 的执行顺序放到最前面,防止其它 Loader 把处理后的代码交给 tslint-loader 去检查
        enforce: 'pre',
      },
    ],
  },
}

结合 stylelint

StyleLintPlugin 能把 stylelint 整合到 Webpack,其使用方法很简单,如下:

const StyleLintPlugin = require('stylelint-webpack-plugin');
module.exports = {
  // ...
  plugins: [
    new StyleLintPlugin(),
  ],
}

一些建议

把代码检查功能整合到 Webpack 中会导致以下问题:

  • 由于执行检查步骤计算量大,整合到 Webpack 中会导致构建变慢;

  • 在整合代码检查到 Webpack 后,输出的错误信息是通过行号来定位错误的,没有编辑器集成显示错误直观;

为了避免以上问题,还可以这样做:

  • 使用集成了代码检查功能的编辑器,让编辑器实时直观地显示错误;

  • 把代码检查步骤放到代码提交时,也就是说在代码提交前去调用以上检查工具去检查代码,只有在检查都通过时才提交代码,这样就能保证提交到仓库的代码都是通过了检查的。

如果你的项目是使用 Git 管理,Git 提供了 Hook 功能能做到在提交代码前触发执行脚本。

husky 可以方便快速地为项目接入 Git Hook, 执行npm i -D husky

安装 husky 时,husky 会通过 Npm Script Hook 自动配置好 Git Hook,你需要做的只是在 package.json 文件中定义几个脚本,方法如下:

{
  "scripts": {
    // 在执行 git commit 前会执行的脚本  
    "precommit": "npm run lint",
    // 在执行 git push 前会执行的脚本  
    "prepush": "lint",
    // 调用 eslint、stylelint 等工具检查代码
    "lint": "eslint && stylelint"
  }
}

precommitprepush 你需要根据自己的情况选择一个,无需两个都设置。

通过 Node.js API 启动 Webpack

Webpack 除了提供可执行的命令行工具外,还提供可在 Node.js 环境中调用的库。 通过 Webpack 暴露的 API,可直接在 Node.js 程序中调用 Webpack 执行构建。

通过 API 去调用并执行 Webpack 比直接通过可执行文件启动更加灵活,可用在一些特殊场景,下面将教你如何使用 Webpack 提供的 API。

Webpack 其实是一个 Node.js 应用程序,它全部通过 JavaScript 开发完成。 在命令行中执行 webpack 命令其实等价于执行 node ./node_modules/webpack/bin/webpack.js

安装和使用 Webpack 模块

在调用 Webpack API 前,需要先安装它:

npm i -D webpack

安装成功后,可以采用以下代码导入 Webpack 模块:

const webpack = require('webpack');
// ES6 语法
import webpack from "webpack";

导出的 webpack 其实是一个函数,使用方法如下:

webpack({
  // Webpack 配置,和 webpack.config.js 文件一致
}, (err, stats) => {
  if (err || stats.hasErrors()) {
    // 构建过程出错
  }
  // 成功执行完构建
});

如果你的 Webpack 配置写在 webpack.config.js 文件中,可以这样使用:

// 读取 webpack.config.js 文件中的配置
const config = require('./webpack.config.js');
webpack(config , callback);

以监听模式运行

以上使用 Webpack API 的方法只能执行一次构建,无法以监听模式启动 Webpack,为了在使用 API 时以监听模式启动,需要获取 Compiler 实例,方法如下:

See the Pen Compiler  by whjin (@whjin) on CodePen.


从以上代码可以看出,从 webpack-dev-middleware 中导出的 webpackMiddleware 是一个函数,该函数需要接收一个 Compiler 实例。Webpack API 导出的 webpack 函数会返回一个Compiler 实例。

webpackMiddleware 函数的返回结果是一个 Expressjs 的中间件,该中间件有以下功能:

  • 接收来自 Webpack Compiler 实例输出的文件,但不会把文件输出到硬盘,而是保存在内存中;

  • 往 Expressjs app 上注册路由,拦截 HTTP 收到的请求,根据请求路径响应对应的文件内容;

通过 webpack-dev-middleware 能够将 DevServer 集成到你现有的 HTTP 服务器中,让你现有的 HTTP 服务器能返回 Webpack 构建出的内容,而不是在开发时启动多个 HTTP 服务器。 这特别适用于后端接口服务采用 Node.js 编写的项目。

Webpack Dev Middleware 支持的配置项

在 Node.js 中调用 webpack-dev-middleware 提供的 API 时,还可以给它传入一些配置项,方法如下:

See the Pen Webpack Dev Middleware by whjin (@whjin) on CodePen.


第2步:修改 HTTP 服务器代码 server.js 文件,接入 webpack-hot-middleware 中间件,修改如下:

See the Pen server.js by whjin (@whjin) on CodePen.