這次帶給大家webpack實戰案例詳解,webpack實戰的注意事項有哪些,下面就是實戰案例,一起來看一下。
查看所有文件頁面:全端開發,以取得更多資訊。快馬加鞭,加班加點,終於把這份文檔整理出來了,順便深入地學習一番,鞏固知識,就是太累人,影響睡眠時間和品質。極客就是想要把事情做到極致,開始了就必須到達終點。
原文連結:webpack實戰,原文廣告模態框遮擋,閱讀體驗不好,所以整理成本文,方便找。
本章教你如何用 Webpack 去解決實際專案中常見的場景。
依照不同場景分割成以下幾類:
使用新語言來開發專案:
使用ES6 語言
使用TypeScript 語言
使用Flow 檢查器
使用Angular2 框架
使用ES6語言
語法用ES5 的prototype
實作。
API 時注入對應的 polyfill 後才能讓低階瀏覽器正常運作。
Babel 是一個 JavaScript 編譯器,能將 ES6 程式碼轉為 ES5 程式碼,讓你使用最新的語言特性而不用擔心相容性問題,並且可以透過插件機制根據需求靈活的擴充功能。
在 Babel 執行編譯的過程中,會從專案根目錄下的
.babelrc 檔案讀取設定
。 .babelrc 是一個 JSON 格式的文件,內容大致如下:<pre class="brush:php;toolbar:false">{
"plugins": [
[
"transform-runtime",
{
"polyfill": false
}
]
],
"presets": [
[
"es2015",
{
"modules": false
}
],
"stage-2",
"react"
]
}</pre>
<h2 style="text-align: left;">Plugins</h2>
<p style="text-align: left;"><code>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
屬性告訴Babel 要轉換的原始碼使用了哪些新的語法特性,一個Presets
對一組新語法特性提供支持,多個Presets
可以疊加。
Presets
其實是一組 Plugins 的集合,每一個 Plugin 完成一個新語法的轉換工作。 Presets 是按照ECMAScript 草案來組織的,通常可以分為以下三大類:
#已經被寫入ECMAScript 標準裡的特性,由於之前每年都有新特性被加入到標準裡;
env 包含目前所有ECMAScript 標準裡的最新特性。
被社群提出來的但還未被寫入ECMAScript 標準裡特性,這其中又分為以下四種:
stage0
只是一個美好激進的想法,有Babel 外掛實現了對這些特性的支持,但不確定是否會被定為標準;
stage1
值得被納入標準的特性;
#stage2
此特性規格已經被起草,將會被納入標準裡;
stage3
此特性規格已定稿,各大瀏覽器廠商與Node.js 社群開始著手實作;
stage4
在接下來的一年將會加入標準裡去。
為了支援一些特定應用場景下的語法,和ECMAScript 標準沒有關係,例如babel-preset-react
是為了支援React 開發中的JSX 語法。
在實際應用程式中,你需要根據專案原始碼所使用的語法去安裝對應的 Plugins 或 Presets。
由於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 可以讓你用更靈活的方式寫 CSS。它是CSS 預處理器,語法和CSS 相似,但加入了變數、邏輯等程式設計元素,程式碼類似這樣:
$blue: #1875e7; p { 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 接入 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 混在一起,可以使用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 是一个 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 检测器由高性能跨平台的 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) {}
有两种方式可以做到这点:
flow-remove-types
可单独使用,速度快。
babel-preset-flow
与 Babel 集成。
由于使用了 Flow 项目一般都会使用 ES6 语法,所以把 Flow 集成到使用 Webpack 构建的项目里最方便的方法是借助 Babel。
安装 npm i -D babel-preset-flow
依赖到项目。
修改 .babelrc
配置文件,加入 Flow Preset:
"presets": [ ...[], "flow" ]
往源码里加入静态类型后重新构建项目,你会发现采用了 Flow 的源码还是能正常在浏览器中运行。
要明确构建的目的只是为了去除源码中的 Flow 静态类型语法,而代码检查和构建无关。 许多编辑器已经整合 Flow,可以实时在代码中高亮指出 Flow 检查出的问题。
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 在不经过转换的情况下也能在浏览器中直接运行。
虽然使用 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 项目的代码特征有 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')
要在使用 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
。
TypeScript 相比于 Babel 的优点在于它原生支持 JSX 语法,你不需要重新安装新的依赖,只需修改一行配置。 但 TypeScript 的不同在于:
使用了 JSX 语法的文件后缀必须是 tsx
。
由于 React 不是采用 TypeScript 编写的,需要安装 react
和 react-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 对项目里的 ts
与 tsx
原文件都采用 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是一个渐进式的 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 组件。
目前最成熟和流行的开发 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 模版的好处在于性能更好。
从 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
在使用 React 框架中,是用最简单的 Hello,Webpack
作为例子让大家理解, 这个例子里因为只输出了一个 bundle.js
文件,所以手写了一个 index.html
文件去引入这个 bundle.js
,才能让应用在浏览器中运行起来。
在实际项目中远比这复杂,一个页面常常有很多资源要加载。接下来举一个实战中的例子,要求如下:
项目采用 ES6 语言加 React 框架。
给页面加入 Google Analytics,这部分代码需要内嵌进 HEAD 标签里去。
给页面加入 Disqus 用户评论,这部分代码需要异步加载以提升首屏加载速度。
压缩和分离 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个文件夹 index
和 login
,把这两个文件夹看成两个单页应用。 并且分别为每个单页应用生成一个 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
都支援。
同構應用程式是指寫一份程式碼但可同時在瀏覽器和伺服器中執行的應用程式。
現在大多數單頁應用程式的視圖都是透過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
,內容如下:
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。
接下来做一个简单的 Electron 应用,要求为应用启动后显示一个主窗口,在主窗口里有一个按钮,点击这个按钮后新显示一个窗口,且使用 React 开发网页。
由于 Electron 应用中的每一个窗口对应一个网页,所以需要开发2个网页,分别是主窗口的 index.html
和新打开的窗口 login.html
。
需要改动的地方如下:
在项目根目录下新建主进程的入口文件 main.js
,内容和上面提到的一致;
主窗口网页的代码如下:
See the Pen main.js by whjin (@whjin) on CodePen.
重新执行构建后,你将会在项目目录下看到一个新目录 lib
,里面放着要发布到 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种离线离线缓存技术,它们分别是:
AppCache 又叫 Application Cache,目前已经从 Web 标准中删除,请尽量不要使用它。
Service Workers 是目前最新的离线缓存技术,是 Web Worker 的一部分。 它通过拦截网络请求实现离线缓存,比 AppCache 更加灵活。它也是构建 PWA 应用的关键技术之一。
Service Workers 是一个在浏览器后台运行的脚本,它生命周期完全独立于网页。它无法直接访问 DOM,但可以通过 postMessage 接口发送消息来和 UI 进程通信。 拦截网络请求是 Service Workers 的一个重要功能,通过它能完成离线缓存、编辑响应、过滤响应等功能。
目前 Chrome、Firefox、Opera 都已经全面支持 Service Workers,但对于移动端浏览器就不太乐观了,只有高版本的 Android 支持。 由于 Service Workers 无法通过注入 polyfill
去实现兼容,所以在你打算使用它前请先调查清楚你的网页的运行场景。
判断浏览器是否支持 Service Workers 的最简单的方法是通过以下代码:
// 如果 navigator 对象上存在 serviceWorker 对象,就表示支持 if (navigator.serviceWorker) { // 通过 navigator.serviceWorker 使用 }
要给网页接入 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 脚本中,引入了新的关键字 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 有如下机制:
每次打开接入了 Service Workers 的网页时,浏览器都会去重新下载 Service Workers 脚本文件(所以要注意该脚本文件不能太大),如果发现和当前已经注册过的文件存在字节差异,就将其视为“新服务工作线程”。
新 Service Workers 线程将会启动,且将会触发其 install
事件。
当网站上当前打开的页面关闭时,旧 Service Workers 线程将会被终止,新 Service Workers 线程将会取得控制权。
新 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 是在安装 Node.js 时附带的包管理器,Npm Script 则是 Npm 内置的一个功能,允许在 package.json
文件里面使用 scripts
字段定义任务:
{ "scripts": { "dev": "node dev.js", "pub": "node build.js" } }
里面的 scripts
字段是一个对象,每一个属性对应一段脚本,以上定义了两个任务 dev
和 pub
。 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 只是一个打包模块化代码的工具,并没有提供任何任务管理相关的功能。 但在实际场景中通常不会是只通过执行 webpack 就能完成所有任务的,而是需要多个任务才能完成。
在开发阶段为了提高开发体验,使用 DevServer 做开发,并且需要输出 Source Map 以方便调试,同时还需要开启自动刷新功能。
为了减小发布到线上的代码尺寸,在构建出发布到线上的代码时,需要压缩输出的代码。
在构建完发布到线上的代码后,需要把构建出的代码提交给发布系统。
可以看出要求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 检查工具是 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 会输出错误原因,例如:
TSLint 是一个和 ESlint 相似的 TypeScript 代码检查工具,区别在于 TSLint 只专注于检查 TypeScript 代码。
TSLint 和 ESlint 的使用方法很相似,首先通过:npm i -g tslint
安装到全局,再去项目根目录下执行:tslint --init
生成配置文件 tslint.json
,在配置好后,再执行:tslint yourfile.ts
去检查 yourfile.ts
文件。
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
文件。
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-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', }, ], }, }
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" } }
precommit
和 prepush
你需要根据自己的情况选择一个,无需两个都设置。
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 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 编写的项目。
在 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.