查看全部文档页面: 全栈开发,获取更多信息。马不停蹄,加班加点,终于把这个文档整理出来了,顺便深刻地学习一番,巩固知识,就是太累人,影响睡眠时间和质量。极客就是想要把事情作到极致,开始了就必须到达终点。css
原文连接:webpack实战,原文广告模态框遮挡,阅读体验很差,因此整理成本文,方便查找。html
本章教你如何用 Webpack 去解决实际项目中常见的场景。前端
按照不一样场景划分红如下几类:vue
使用新语言来开发项目:node
使用新框架来开发项目:react
用 Webpack 构建单页应用:webpack
用 Webpack 构建不一样运行环境的项目:git
Webpack 结合其它工具搭配使用,各取所长:github
用 Webpack 加载特殊类型的资源:web
因为本文不推荐使用TypeScript,ES6就足够完成大部分任务。原文连接:使用 TypeScript 语言
Angular2不在个人技术栈范围,因此这一章不加入,有兴趣的查看原文:使用 Angular2 框架
一般咱们须要把采用 ES6 编写的代码转换成目前已经支持良好的 ES5 代码,这包含2件事:
class
语法用 ES5 的 prototype
实现。fetch
API 时注入对应的 polyfill 后才能让低端浏览器正常运行。Babel 能够方便的完成以上2件事。
Babel 是一个 JavaScript 编译器,能将 ES6 代码转为 ES5 代码,让你使用最新的语言特性而不用担忧兼容性问题,而且能够经过插件机制根据需求灵活的扩展。
在 Babel 执行编译的过程当中,会从项目根目录下的 .babelrc
文件读取配置。.babelrc
是一个 JSON 格式的文件,内容大体以下:
{ "plugins": [ [ "transform-runtime", { "polyfill": false } ] ], "presets": [ [ "es2015", { "modules": false } ], "stage-2", "react" ] }
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 标准里的特性,因为以前每一年都有新特性被加入到标准里;
被社区提出来的但还未被写入 ECMAScript 标准里特性,这其中又分为如下四种:
stage0
只是一个美好激进的想法,有 Babel 插件实现了对这些特性的支持,可是不肯定是否会被定为标准;stage1
值得被归入标准的特性;stage2
该特性规范已经被起草,将会被归入标准里;stage3
该特性规范已经定稿,各大浏览器厂商和 Node.js 社区开始着手实现;stage4
在接下来的一年将会加入到标准里去。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; 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 接入 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 的不一样在于:
tsx
。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
,才能让应用在浏览器中运行起来。
在实际项目中远比这复杂,一个页面经常有不少资源要加载。接下来举一个实战中的例子,要求以下:
在开始前先来看看该应用最终发布到线上的代码。
能够看到部分代码被内嵌进了 HTML 的 HEAD 标签中,部分文件的文件名称被打上根据文件内容算出的 Hash 值,而且加载这些文件的 URL 地址也被正常的注入到了 HTML 中。
推荐一个用于方便地解决以上问题的 Webpack 插件 web-webpack-plugin。 该插件已经被社区上许多人使用和验证,解决了你们的痛点得到了不少好评,下面具体介绍如何用它来解决上面的问题。
首先,修改 Webpack 配置。
以上配置中,大多数都是按照前面已经讲过的内容增长的配置,例如:
NODE_ENV
环境变量为 production
,以去除源码中只有开发时才须要的部分;但最核心的部分在于 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://dive-into-webpack.disqus.com/embed.js" async></script> </head> <body> <div id="app"></div> <!--导入 Chunk app 中的 JS--> <script src="app"></script> <!--Disqus 评论容器--> <div id="disqus_thread"></div> </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
插件还支持一些其它更高级的用法,详情能够访问该项目主页阅读文档。
在开始前先来看看该应用最终发布到线上的代码。
<html> <head> <meta charset="UTF-8"> <!--从多个页面中抽离出的公共 CSS 代码--> <link rel="stylesheet" href="common_7cc98ad0.css"> <!--只有这个页面须要的 CSS 代码--> <link rel="stylesheet" href="login_e31e214b.css"> <!--注入 google_analytics 中的 JS 代码--> <script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); ga('create', 'UA-XXXXX-Y', 'auto'); ga('send', 'pageview');</script> <!--异步加载 Disqus 评论--> <script async="" src="https://dive-into-webpack.disqus.com/embed.js"></script> </head> <body> <div id="app"></div> <!--从多个页面中抽离出的公共 JavaScript 代码--> <script src="common_a1d9142f.js"></script> <!--只有这个页面须要的 JavaScript 代码--> <script src="login_f926c4e6.js"></script> <!--Disqus 评论容器--> <div id="disqus_thread"></div> </body> </html>
构建出的目录结构为:
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 配置文件修改以下:
<p data-height="465" data-theme-id="0" data-slug-hash="gzJWwB" data-default-tab="js,result" data-user="whjin" data-embed-version="2" data-pen-title="webpack管理多个单页应用" class="codepen">See the Pen webpack管理多个单页应用 by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
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://dive-into-webpack.disqus.com/embed.js" async></script> </head> <body> <div id="app"></div> <!--在这注入该页面所依赖但没有手动导入的 JavaScript--> <!--SCRIPT--> <!--Disqus 评论容器--> <div id="disqus_thread"></div> </body> </html>
因为这个模版文件被看成项目中全部单页应用的模版,就不能再像上一节中直接写 Chunk 的名称去引入资源,由于须要被注入到当前页面的 Chunk 名称是不定的,每一个单页应用都会有本身的名称。 <!--STYLE-->
和 <!--SCRIPT-->
的做用在于保证该页面所依赖的资源都会被注入到生成的 HTML 模版里去。
web-webpack-plugin
能分析出每一个页面依赖哪些资源,例如对于 login.html
来讲,插件能够肯定该页面依赖如下资源:
common.css
;common.js
;login.css
;login.css
。因为模版文件 template.html
里没有指出引入这些依赖资源的 HTML 语句,插件会自动将没有手动导入但页面依赖的资源按照不一样类型注入到 <!--STYLE-->
和 <!--SCRIPT-->
所在的位置。
<!--STYLE-->
所在的位置,若是 <!--STYLE-->
不存在就注入到 HTML HEAD 标签的最后;<!--SCRIPT-->
所在的位置,若是 <!--SCRIPT-->
不存在就注入到 HTML BODY 标签的最后。若是后续有新的页面须要开发,只须要在 pages
目录下新建一个目录,目录名称取为输出 HTML 文件的名称,目录下放这个页面相关的代码便可,无需改动构建代码。
因为 AutoWebPlugin
是间接的经过上一节提到的 WebPlugin
实现的,WebPlugin
支持的功能 AutoWebPlugin
都支持。
同构应用是指写一份代码但可同时在浏览器和服务器中运行的应用。
如今大多数单页应用的视图都是经过 JavaScript 代码在浏览器端渲染出来的,但在浏览器端渲染的坏处有:
为了解决以上问题,有人提出可否将本来只运行在浏览器中的 JavaScript 渲染代码也在服务器端运行,在服务器端渲染出带内容的 HTML 后再返回。 这样就能让搜索引擎爬虫直接抓取到带数据的 HTML,同时也能下降首屏渲染时间。 因为 Node.js 的流行和成熟,以及虚拟 DOM 提出与实现,使这个假设成为可能。
实际上如今主流的前端框架都支持同构,包括 React、Vue二、Angular2,其中最早支持也是最成熟的同构方案是 React。 因为 React 使用者更多,它们之间又很类似,本节只介绍如何用 Webpack 构建 React 同构应用。
同构应用运行原理的核心在于虚拟 DOM,虚拟 DOM 的意思是不直接操做 DOM 而是经过 JavaScript Object 去描述本来的 DOM 结构。 在须要更新 DOM 时不直接操做 DOM 树,而是经过更新 JavaScript Object 后再映射成 DOM 操做。
虚拟 DOM 的优势在于:
以 React 为例,核心模块 react 负责管理 React 组件的生命周期,而具体的渲染工做能够交给 react-dom
模块来负责。
react-dom
在渲染虚拟 DOM 树时有2中方式可选:
render()
函数去操做浏览器 DOM 树来展现出结果。renderToString()
计算出表示虚拟 DOM 的 HTML 形式的字符串。构建同构应用的最终目的是从一份项目源码中构建出2份 JavaScript 代码,一份用于在浏览器端运行,一份用于在 Node.js 环境中运行渲染出 HTML。 其中用于在 Node.js 环境中运行的 JavaScript 代码须要注意如下几点:
document
进行 DOM 操做,由于 Node.js 不支持这些 API;node_modules
里的第三方模块和 Node.js 原生模块(例如 fs
模块)打包进去,而是须要经过 CommonJS 规范去引入这些模块。用于构建浏览器环境代码的 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 <h1>Hello,Webpack</h1> } }
分别为不一样环境的渲染入口写两份不一样的文件,分别是用于浏览器端渲染 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(<AppComponent/>, 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(` <html> <head> <meta charset="UTF-8"> </head> <body> <div id="app">${render()}</div> <!--导入 Webpack 输出的用于浏览器端渲染的 JS 文件--> <script src="./dist/bundle_browser.js"></script> </body> </html> `); }); // 其它请求路径返回对应的本地文件 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 的包了,抓包效果图以下:
能够看到服务器返回的是渲染出内容后的 HTML 而不是 HTML 模版,这说明同构应用的改造完成。
本实例提供 项目完整代码
Electron 是 Node.js 和 Chromium 浏览器的结合体,用 Chromium 浏览器显示出的 Web 页面做为应用的 GUI,经过 Node.js 去和操做系统交互。 当你在 Electron 应用中的一个窗口操做时,其实是在操做一个网页。当你的操做须要经过操做系统去完成时,网页会经过 Node.js 去和操做系统交互。
采用这种方式开发桌面端应用的优势有:
在运行 Electron 应用时,会从启动一个主进程开始。主进程的启动是经过 Node.js 去执行一个入口 JavaScript 文件实现的,这个入口文件 main.js
内容以下:
<p data-height="565" data-theme-id="0" data-slug-hash="vjweQv" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="Electron-main.js" class="codepen">See the Pen Electron-main.js by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
主进程启动后会一直驻留在后台运行,你眼睛所看得的和操做的窗口并非主进程,而是由主进程新启动的窗口子进程。
应用从启动到退出有一系列生命周期事件,经过 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
,内容和上面提到的一致;import React, { Component } from 'react'; import { render } from 'react-dom'; import { remote } from 'electron'; import path from 'path'; import './index.css'; class App extends Component { // 在按钮被点击时 handleBtnClick() { // 新窗口对应的页面的 URI 地址 const modalPath = path.join('file://', remote.app.getAppPath(), 'dist/login.html'); // 新窗口的大小 let win = new remote.BrowserWindow({ width: 400, height: 320 }) win.on('close', function () { // 窗口被关闭时清空资源 win = null }) // 加载网页 win.loadURL(modalPath) // 显示窗口 win.show() } render() { return ( <div> <h1>Page Index</h1> <button onClick={this.handleBtnClick}>Open Page Login</button> </div> ) } } render(<App/>, window.document.getElementById('app'));
其中最关键的部分在于在按钮点击事件里经过 electron
库里提供的 API 去新打开一个窗口,并加载网页文件所在的地址。
页面部分的代码已经修改完成,接下来修改构建方面的代码。 这里构建须要作到如下几点:
要完成以上要求很是简单,由于 Webpack 内置了对 Electron 的支持。 只须要给 Webpack 配置文件加上一行代码便可,以下:
target: 'electron-renderer',
以上修改都完成后从新执行 Webpack 构建,对应的网页须要的代码都输出到了项目根目录下的 dist
目录里。
为了以 Electron 应用的形式运行,还须要安装新依赖:
# 安装 Electron 执行环境到项目中 npm i -D electron
发布到 Npm 仓库的模块有如下几个特色:
package.json
文件。该文件描述了模块的入口文件是哪一个,该模块又依赖哪些模块等。Webpack 不只可用于构建运行的应用,也可用于构建上传到 Npm 的模块。 接下来用教你们如何用 Webpack 构建一个可上传的 Npm 仓库的 React 组件,具体要求以下:
在开始前先看下最终发布到 Npm 仓库的模块的目录结构:
node_modules/hello-webpack ├── lib │ ├── index.css (组件全部依赖的 CSS 都在这个文件中) │ ├── index.css.map │ ├── index.js (符合 CommonJS 模块化规范的 ES5 代码) │ └── index.js.map ├── src (ES6 源码) │ ├── index.css │ └── index.js └── package.json (模块描述文件)
src/index.js
文件,内容以下:
import React, { Component } from 'react'; import './index.css'; // 导出该组件供给其它模块使用 export default class HelloWebpack extends Component { render() { return <h1 className="hello-component">Hello,Webpack</h1> } }
要使用该模块时只须要这样:
// 经过 ES6 语法导入 import HelloWebpack from 'hello-webpack'; import 'hello-webpack/lib/index.css'; // 或者经过 ES5 语法导入 var HelloWebpack = require('hello-webpack'); require('hello-webpack/lib/index.css'); // 使用 react-dom 渲染 render(<HelloWebpack/>);
对于要求1,能够这样作到:
babel-loader
把 ES6 代码转换成 ES5 的代码。devtool: 'source-map'
输出 Source Map 以发布调试。output.libraryTarget='commonjs2'
使输出的代码符合CommonJS2 模块化规范,以供给其它模块导入使用。相关的 Webpack 配置代码以下:
module.exports = { output: { // 输出的代码符合 CommonJS 模块化规范,以供给其它模块导入使用。 libraryTarget: 'commonjs2', }, // 输出 Source Map devtool: 'source-map', };
对于要求2,须要经过 css-loader
和 extract-text-webpack-plugin
实现,相关的 Webpack 配置代码以下:
const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { module: { rules: [ { // 增长对 CSS 文件的支持 test: /\.css/, // 提取出 Chunk 中的 CSS 代码到单独的文件中 use: ExtractTextPlugin.extract({ use: ['css-loader'] }), }, ] }, plugins: [ new ExtractTextPlugin({ // 输出的 CSS 文件名称 filename: 'index.css', }), ], };
此步引入了3个新依赖:
# 安装 Webpack 构建所须要的新依赖 npm i -D style-loader css-loader extract-text-webpack-plugin
对于要求3,须要注意的是 Babel 在把 ES6 代码转换成 ES5 代码时会注入一些辅助函数。
例以下面这段 ES6 代码:
class HelloWebpack extends Component{ }
在被转换成能正常运行的 ES5 代码时须要如下2个辅助函数:
babel-runtime/helpers/createClass
用于实现 class
语法babel-runtime/helpers/inherits
用于实现 extends
语法默认的状况下 Babel 会在每一个输出文件中内嵌这些依赖的辅助函数的代码,若是多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会重复的出现不少次,形成代码冗余。
为了避免让这些辅助函数的代重复出现,能够在依赖它们的时候经过 require('babel-runtime/helpers/createClass')
的方式去导入,这样就能作到只让它们出现一次。 babel-plugin-transform-runtime
插件就是用来作这个事情的。
修改 .babelrc
文件,为其加入 transform-runtime
插件:
{ "plugins": [ [ "transform-runtime", { // transform-runtime 默认会自动的为你使用的 ES6 API 注入 polyfill // 假如你在源码中使用了 Promise,输出的代码将会自动注入 require('babel-runtime/core-js/Promise') 语句 // polyfill 的注入应该交给模块使用者,由于使用者可能在其它地方已经注入了其它的 Promise polyfill 库 // 因此关闭该功能 "polyfill": false } ] ] }
因为加入 babel-plugin-transform-runtime
后生成的代码中会大量出现相似 require('babel-runtime/helpers/createClass')
这样的语句,因此输出的代码将依赖 babel-runtime
模块。
此步引入了3个新依赖:
# 安装 Webpack 构建所须要的新依赖 npm i -D babel-plugin-transform-runtime # 安装输出代码运行时所需的新依赖 npm i -S babel-runtime
对于要求4,须要经过在 其它配置项 中介绍过的 Externals
来实现。
Externals 用来告诉 Webpack 要构建的代码中使用了哪些不用被打包的模块,也就是说这些模版是外部环境提供的,Webpack 在打包时能够忽略它们。
相关的 Webpack 配置代码以下:
module.exports = { // 经过正则命中全部以 react 或者 babel-runtime 开头的模块 // 这些模块经过注册在运行环境中的全局变量访问,不用被重复打包进输出的代码里 externals: /^(react|babel-runtime)/, };
开启以上配置后,输出的代码中会存在导入 react
或者 babel-runtime
模块的代码,可是它们的 react
或者 babel-runtime
的内容不会被包含进去,以下:
[ (function (module, exports) { module.exports = require("babel-runtime/helpers/inherits"); }), (function (module, exports) { module.exports = require("react"); }) ]
这样就作到了在保持代码正确性的状况下,输出文件不存放 react 或者 babel-runtime 模块的代码。
实际上当你在开发 Npm 模块时,不仅须要对 react 和 babel-runtime 模块作这样的处理,而是须要对全部正在开发的模块所依赖的模块进行这样的处理。 由于正在开发的模块所依赖的模块也可能被其它模块所依赖。 当一个项目中一个模块被依赖屡次时,Webpack 只会将其打包一次。
完成以上4步后最终的 Webpack 完整配置代码以下:
const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { // 模块的入口文件 entry: './src/index.js', output: { // 输出文件的名称 filename: 'index.js', // 输出文件的存放目录 path: path.resolve(__dirname, 'lib'), // 输出的代码符合 CommonJS 模块化规范,以供给其它模块导入使用。 libraryTarget: 'commonjs2', }, // 经过正则命中全部以 react 或者 babel-runtime 开头的模块, // 这些模块使用外部的,不能被打包进输出的代码里,防止它们出现屡次。 externals: /^(react|babel-runtime)/, module: { rules: [ { test: /\.js$/, use: ['babel-loader'], // 排除 node_modules 目录下的文件, // node_modules 目录下的文件都是采用的 ES5 语法,不必再经过 Babel 去转换。 exclude: path.resolve(__dirname, 'node_modules'), }, { // 增长对 CSS 文件的支持 test: /\.css/, // 提取出 Chunk 中的 CSS 代码到单独的文件中 use: ExtractTextPlugin.extract({ use: ['css-loader'] }), }, ] }, plugins: [ new ExtractTextPlugin({ // 输出的 CSS 文件名称 filename: 'index.css', }), ], // 输出 Source Map devtool: 'source-map', };
从新执行构建后,你将会在项目目录下看到一个新目录 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种离线离线缓存技术,它们分别是:
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 有以下机制:
install
事件。新 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 脚本代码以下:
// 当前缓存版本的惟一标识符,用当前时间代替 var cacheKey = new Date().toISOString(); // 当前缓存白名单,在新脚本的 install 事件里将使用白名单里的 key var cacheWhitelist = [cacheKey]; // 须要被缓存的文件的 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 线程取得控制权后,将会触发其 activate 事件 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); } }) ); }) ); });
用 Webpack 构建接入 Service Workers 的离线应用要解决的关键问题在于如何生成上面提到的 sw.js
文件, 而且sw.js
文件中的 cacheFileList
变量,表明须要被缓存文件的 URL 列表,须要根据输出文件列表所对应的 URL 来决定,而不是像上面那样写成静态值。
假如构建输出的文件目录结构为:
├── app_4c3e186f.js ├── app_7cc98ad0.css └── index.html
那么 sw.js
文件中 cacheFileList
的值应该是:
var cacheFileList = [ '/index.html', 'app_4c3e186f.js', 'app_7cc98ad0.css' ];
Webpack 没有原生功能能完成以上要求,幸亏庞大的社区中已经有人为咱们作好了一个插件 serviceworker-webpack-plugin 能够方便的解决以上问题。 使用该插件后的 Webpack 配置以下:
const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const { WebPlugin } = require('web-webpack-plugin'); const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin'); module.exports = { entry: { app: './main.js'// Chunk app 的 JS 执行入口文件 }, output: { filename: '[name].js', publicPath: '', }, module: { rules: [ { test: /\.css/,// 增长对 CSS 文件的支持 // 提取出 Chunk 中的 CSS 代码到单独的文件中 use: ExtractTextPlugin.extract({ use: ['css-loader'] // 压缩 CSS 代码 }), }, ] }, plugins: [ // 一个 WebPlugin 对应一个 HTML 文件 new WebPlugin({ template: './template.html', // HTML 模版文件所在的文件路径 filename: 'index.html' // 输出的 HTML 的文件名称 }), new ExtractTextPlugin({ filename: `[name].css`,// 给输出的 CSS 文件名称加上 Hash 值 }), new ServiceWorkerWebpackPlugin({ // 自定义的 sw.js 文件所在路径 // ServiceWorkerWebpackPlugin 会把文件列表注入到生成的 sw.js 中 entry: path.join(__dirname, 'sw.js'), }), ], devServer: { // Service Workers 依赖 HTTPS,使用 DevServer 提供的 HTTPS 功能。 https: true, } };
以上配置有2点须要注意:
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 就能完成全部任务的,而是须要多个任务才能完成。
能够看出要求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 通常经过人去执行,而检查代码是经过机器去执行一些自动化的检查。 自动化的检查代码成本更低,实施代价更小。
检查代码主要检查如下几项:
目前已经有成熟的工具能够检验诸如 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 中会致使如下问题:
为了不以上问题,还能够这样作:
若是你的项目是使用 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 实例,方法以下:
// 若是不传 callback 回调函数,就会返回一个 Compiler 实例,用于让你去控制启动,而不是像上面那样当即启动 const compiler = webpack(config); // 调用 compiler.watch 以监听模式启动,返回的 watching 用于关闭监听 const watching = compiler.watch({ // watchOptions aggregateTimeout: 300, },(err, stats)=>{ // 每次因文件发生变化而从新执行完构建后 }); // 调用 watching.close 关闭监听 watching.close(()=>{ // 在监听关闭后 });
DevServer 是一个方便开发的小型 HTTP 服务器, DevServer 实际上是基于 webpack-dev-middleware 和 Expressjs 实现的, 而 webpack-dev-middleware 实际上是 Expressjs 的一个中间件。
也就是说,实现 DevServer 基本功能的代码大体以下:
const express = require('express'); const webpack = require('webpack'); const webpackMiddleware = require('webpack-dev-middleware'); // 从 webpack.config.js 文件中读取 Webpack 配置 const config = require('./webpack.config.js'); // 实例化一个 Expressjs app const app = express(); // 用读取到的 Webpack 配置实例化一个 Compiler const compiler = webpack(config); // 给 app 注册 webpackMiddleware 中间件 app.use(webpackMiddleware(compiler)); // 启动 HTTP 服务器,服务器监听在 3000 端口 app.listen(3000);
从以上代码能够看出,从 webpack-dev-middleware
中导出的 webpackMiddleware
是一个函数,该函数须要接收一个 Compiler 实例。Webpack API 导出的 webpack
函数会返回一个Compiler 实例。
webpackMiddleware
函数的返回结果是一个 Expressjs 的中间件,该中间件有如下功能:
经过 webpack-dev-middleware
可以将 DevServer 集成到你现有的 HTTP 服务器中,让你现有的 HTTP 服务器能返回 Webpack 构建出的内容,而不是在开发时启动多个 HTTP 服务器。 这特别适用于后端接口服务采用 Node.js 编写的项目。
在 Node.js 中调用 webpack-dev-middleware 提供的 API 时,还能够给它传入一些配置项,方法以下:
// webpackMiddleware 函数的第二个参数为配置项 app.use(webpackMiddleware(compiler, { // webpack-dev-middleware 全部支持的配置项 // 只有 publicPath 属性为必填,其它都是选填项 // Webpack 输出资源绑定在 HTTP 服务器上的根目录, // 和 Webpack 配置中的 publicPath 含义一致 publicPath: '/assets/', // 不输出 info 类型的日志到控制台,只输出 warn 和 error 类型的日志 noInfo: false, // 不输出任何类型的日志到控制台 quiet: false, // 切换到懒惰模式,这意味着不监听文件变化,只会在请求到时再去编译对应的文件, // 这适合页面很是多的项目。 lazy: true, // watchOptions // 只在非懒惰模式下才有效 watchOptions: { aggregateTimeout: 300, poll: true }, // 默认的 URL 路径, 默认是 'index.html'. index: 'index.html', // 自定义 HTTP 头 headers: {'X-Custom-Header': 'yes'}, // 给特定文件后缀的文件添加 HTTP mimeTypes ,做为文件类型映射表 mimeTypes: {'text/html': ['phtml']}, // 统计信息输出样式 stats: { colors: true }, // 自定义输出日志的展现方法 reporter: null, // 开启或关闭服务端渲染 serverSideRender: false, }));
DevServer 提供了一个方便的功能,能够作到在监听到文件发生变化时自动替换网页中的老模块,以作到实时预览。
DevServer 虽然是基于 webpack-dev-middleware
实现的,但 webpack-dev-middleware
并无实现模块热替换功能,而是 DevServer 本身实现了该功能。
为了在使用 webpack-dev-middleware
时也能使用模块热替换功能去提高开发效率,须要额外的再接入 webpack-hot-middleware。 须要作如下修改才能实现模块热替换。
第1步:修改 webpack.config.js
文件,加入 HotModuleReplacementPlugin
插件,修改以下:
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin'); module.exports = { entry: [ // 为了支持模块热替换,注入代理客户端 'webpack-hot-middleware/client', // JS 执行入口文件 './src/main.js' ], output: { // 把全部依赖的模块合并输出到一个 bundle.js 文件 filename: 'bundle.js', }, plugins: [ // 为了支持模块热替换,生成 .hot-update.json 文件 new HotModuleReplacementPlugin(), ], devtool: 'source-map', };
第2步:修改 HTTP 服务器代码 server.js
文件,接入 webpack-hot-middleware
中间件,修改以下:
const express = require('express'); const webpack = require('webpack'); const webpackMiddleware = require('webpack-dev-middleware'); // 从 webpack.config.js 文件中读取 Webpack 配置 const config = require('./webpack.config.js'); // 实例化一个 Expressjs app const app = express(); // 用读取到的 Webpack 配置实例化一个 Compiler const compiler = webpack(config); // 给 app 注册 webpackMiddleware 中间件 app.use(webpackMiddleware(compiler)); // 为了支持模块热替换,响应用于替换老模块的资源 app.use(require('webpack-hot-middleware')(compiler)); // 把项目根目录做为静态资源目录,用于服务 HTML 文件 app.use(express.static('.')); // 启动 HTTP 服务器,服务器监听在 3000 端口 app.listen(3000, () => { console.info('成功监听在 3000'); });
第3步:修改执行入口文件 main.js
,加入替换逻辑,在文件末尾加入如下代码:
if (module.hot) { module.hot.accept(); }
第4步:安装新引入的依赖:
npm i -D webpack-dev-middleware webpack-hot-middleware express
安装成功后,经过 node ./server.js
就能启动一个相似于 DevServer 那样支持模块热替换的自定义 HTTP 服务了。
本实例提供 项目完整代码
在网页中不可避免的会依赖图片资源,例如 PNG、JPG、GIF,下面来教你如何用 Webpack 加载图片资源。
file-loader
file-loader 能够把 JavaScript 和 CSS 中导入图片的语句替换成正确的地址,并同时把文件输出到对应的位置。
例如 CSS 源码是这样写的:
#app { background-image: url(./imgs/a.png); }
被 file-loader
转换后输出的 CSS 会变成这样:
#app { background-image: url(5556e1251a78c5afda9ee7dd06ad109b.png); }
而且在输出目录 dist
中也多出 ./imgs/a.png
对应的图片文件 hash.png
, 输出的文件名是根据文件内容的计算出的 Hash 值。
同理在 JavaScript 中导入图片的源码以下:
import imgB from './imgs/b.png'; window.document.getElementById('app').innerHTML = ` <img src="${imgB}"/> `;
通过 file-loader
处理后输出的 JavaScript 代码以下:
module.exports = __webpack_require__.p + "0bcc1f8d385f78e1271ebfca50668429.png";
也就是说 imgB
的值就是图片对应的 URL 地址。
在 Webpack 中使用 file-loader
很是简单,相关配置以下:
module.exports = { module: { rules: [ { test: /\.png$/, use: ['file-loader'] } ] } };
url-loader
url-loader 能够把文件的内容通过 base64
编码后注入到 JavaScript 或者 CSS 中去。
例如 CSS 源码是这样写的:
#app { background-image: url(./imgs/a.png); }
被 url-loader
转换后输出的 CSS 会变成这样:
#app { background-image: url(data:image/png;base64,iVBORw01afer...); /* 结尾省略了剩下的 base64 编码后的数据 */ }
同理在 JavaScript 中效果也相似。
从上面的例子中能够看出 url-loader
会把根据图片内容计算出的 base64
编码的字符串直接注入到代码中,因为通常的图片数据量巨大,这会致使 JavaScript、CSS 文件也跟着变大。 因此在使用 url-loader
时必定要注意图片体积不能太大,否则会致使 JavaScript、CSS 文件过大而带来的网页加载缓慢问题。
通常利用 url-loader
把网页须要用到的小图片资源注入到代码中去,以减小加载次数。由于在 HTTP/1 协议中,每加载一个资源都须要创建一次 HTTP 连接, 为了一个很小的图片而新建一次 HTTP 链接是不划算的。
url-loader
考虑到了以上问题,并提供了一个方便的选择 limit
,该选项用于控制当文件大小小于 limit
时才使用 url-loader
,不然使用 fallback
选项中配置的 loader
。 相关 Webpack 配置以下:
module.exports = { module: { rules: [ { test: /\.png$/, use: [{ loader: 'url-loader', options: { // 30KB 如下的文件采用 url-loader limit: 1024 * 30, // 不然采用 file-loader,默认值就是 file-loader fallback: 'file-loader', } }] } ] }, };
除此以外,你还能够作如下优化:
以上加载图片的方法一样适用于其它二进制类型的资源,例如 PDF、SWF 等等。
本实例提供 项目完整代码
SVG 做为矢量图的一种标准格式,已经获得了各大浏览器的支持,它也成为了 Web 中矢量图的代名词。 在网页中采用 SVG 代替位图有以下好处:
画图工具能导出一个个 .svg
文件,SVG 的导入方法和图片相似,既能够像下面这样在 CSS 中直接使用:
body { background-image: url(./svgs/activity.svg); }
也能够在 HTML 中使用:
<img src="./svgs/activity.svg"/>
也就是说能够直接把 SVG 文件当成一张图片来使用,方法和使用图片时彻底同样。
使用 file-loader
和使用 url-loader
对 SVG 来讲一样有效,只须要把 Loader test 配置中的文件后缀改为 .svg
,代码以下:
module.exports = { module: { rules: [ { test: /\.svg/, use: ['file-loader'] } ] }, };
因为 SVG 是文本格式的文件,除了以上两种方法外还有其它方法,下面来一一说明。
raw-loader
raw-loader 能够把文本文件的内容读取出来,注入到 JavaScript 或 CSS 中去。
例如在 JavaScript 中这样写:
import svgContent from './svgs/alert.svg';
通过 raw-loader
处理后输出的代码以下:
module.exports = "<svg xmlns=\"http://www.w3.org/2000/svg\"... </svg>" // 末尾省略 SVG 内容
也就是说 svgContent
的内容就等于字符串形式的 SVG,因为 SVG 自己就是 HTML 元素,在获取到 SVG 内容后,能够直接经过如下代码将 SVG 插入到网页中:
window.document.getElementById('app').innerHTML = svgContent;
使用 raw-loader
时相关的 Webpack 配置以下:
module.exports = { module: { rules: [ { test: /\.svg$/, use: ['raw-loader'] } ] } };
因为 raw-loader
会直接返回 SVG 的文本内容,而且没法经过 CSS 去展现 SVG 的文本内容,所以采用本方法后没法在 CSS 中导入 SVG。 也就是说在 CSS 中不能够出现 background-image: url(./svgs/activity.svg)
这样的代码,由于 background-image: url(<svg>...</svg>)
是不合法的。
svg-inline-loader
svg-inline-loader 和上面提到的 raw-loader
很是类似, 不一样在于 svg-inline-loader
会分析 SVG 的内容,去除其中没必要要的部分代码,以减小 SVG 的文件大小。
在使用画图工具如 Adobe Illustrator、Sketch 制做 SVG 后,在导出时这些工具会生成对网页运行来讲没必要要的代码。 举个例子,如下是 Sketch 导出的 SVG 的代码:
<svg class="icon" verison="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="#000"> <circle cx="12" cy="12" r="10"/> </svg>
被 svg-inline-loader
处理后会精简成以下:
<svg viewBox="0 0 24 24" stroke="#000"><circle cx="12" cy="12" r="10"/></svg>
也就是说 svg-inline-loader
增长了对 SVG 的压缩功能。
使用 svg-inline-loader
时相关的 Webpack 配置以下:
module.exports = { module: { rules: [ { test: /\.svg$/, use: ['svg-inline-loader'] } ] } };
因为在开发过程当中常常会使用新语言去开发项目,最后会把源码转换成能在浏览器中直接运行的 JavaScript 代码。 这样作虽能提高开发效率,在调试代码的过程当中你会发现生成的代码可读性很是差,这给代码调试带来了不便。
Webpack 支持为转换生成的代码输出对应的 Source Map 文件,以方便在浏览器中能经过源码调试。 控制 Source Map 输出的 Webpack 配置项是 devtool
,它有不少选项,下面来一一详细介绍。
devtool | 含义 |
---|---|
空 | 不生成 Source Map |
eval |
每一个 module 会封装到 eval 里包裹起来执行,而且会在每一个 eval 语句的末尾追加注释 //# sourceURL=webpack:///./main.js |
source-map |
会额外生成一个单独 Source Map 文件,而且会在 JavaScript 文件末尾追加 //# sourceMappingURL=bundle.js.map |
hidden-source-map |
和 source-map 相似,但不会在 JavaScript 文件末尾追加 //# sourceMappingURL=bundle.js.map |
inline-source-map |
和 source-map 相似,但不会额外生成一个单独 Source Map 文件,而是把 Source Map 转换成 base64 编码内嵌到 JavaScript 中 |
eval-source-map |
和 eval 相似,但会把每一个模块的 Source Map 转换成 base64 编码内嵌到 eval 语句的末尾,例如 //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW... |
cheap-source-map |
和 source-map 相似,但生成的 Source Map 文件中没有列信息,所以生成速度更快 |
cheap-module-source-map |
和 cheap-source-map 相似,但会包含 Loader 生成的 Source Map |
其实以上表格只是列举了 devtool
可能取值的一部分, 它的取值其实能够由 source-map
、eval
、inline
、hidden
、cheap
、module
这六个关键字随意组合而成。 这六个关键字每一个都表明一种特性,它们的含义分别是:
eval
:用 eval
语句包裹须要安装的模块;source-map
:生成独立的 Source Map 文件;hidden
:不在 JavaScript 文件中指出 Source Map 文件所在,这样浏览器就不会自动加载 Source Map;inline
:把生成的 Source Map 转换成 base64
格式内嵌在 JavaScript 文件中;cheap
:生成的 Source Map 中不会包含列信息,这样计算量更小,输出的 Source Map 文件更小;同时 Loader 输出的 Source Map 不会被采用;module
:来自 Loader 的 Source Map 被简单处理成每行一个模块;若是你不关心细节和性能,只是想在不出任何差错的状况下调试源码,能够直接设置成 source-map
,但这样会形成两个问题:
source-map
模式下会输出质量最高最详细的 Source Map,这会形成构建速度缓慢,特别是在开发过程须要频繁修改的时候会增长等待时间;source-map
模式下会把 Source Map 暴露出去,若是构建发布到线上的代码的 Source Map 暴露出去就等于源码被泄露;为了解决以上两个问题,能够这样作:
devtool
设置成 cheap-module-eval-source-map
,由于生成这种 Source Map 的速度最快,能加速构建。因为在开发环境下不会作代码压缩,Source Map 中即便没有列信息也不会影响断点调试;devtool
设置成 hidden-source-map
,意思是生成最详细的 Source Map,但不会把 Source Map 暴露出去。因为在生产环境下会作代码压缩,一个 JavaScript 文件只有一行,因此须要列信息。在生产环境下一般不会把 Source Map 上传到 HTTP 服务器让用户获取,而是上传到 JavaScript 错误收集系统,在错误收集系统上根据 Source Map 和收集到的 JavaScript 运行错误堆栈计算出错误所在源码的位置。不要在生产环境下使用
inline
模式的 Source Map, 由于这会使 JavaScript 文件变得很大,并且会泄露源码。
有些从 Npm 安装的第三方模块是采用 ES6 或者 TypeScript 编写的,它们在发布时会同时带上编译出来的 JavaScript 文件和对应的 Source Map 文件,以方便你在使用它们出问题的时候调试它们;
默认状况下 Webpack 是不会去加载这些附加的 Source Map 文件的,Webpack 只会在转换过程当中生成 Source Map。 为了让 Webpack 加载这些附加的 Source Map 文件,须要安装 source-map-loader 。 使用方法以下:
module.exports = { module: { rules: [ { test: /\.js$/, // 只加载你关心的目录下的 Source Map,以提高构建速度 include: [path.resolve(root, 'node_modules/some-components/')], use: ['source-map-loader'], // 要把 source-map-loader 的执行顺序放到最前面,若是在 source-map-loader 以前有 Loader 转换了该 JavaScript 文件,会致使 Source Map 映射错误 enforce: 'pre' } ] } };
因为source-map-loader
在加载 Source Map 时计算量很大,所以要避免让该 Loader 处理过多的文件,否则会致使构建速度缓慢。 一般会采用include
去命中只关心的文件。
再安装新引入的依赖:
npm i -D source-map-loader
重启 Webpack 后,你就能在浏览器中调试 node_modules/some-components/
目录下的源码了。
在实际应用中,会遇到各类各样的需求,虽然前面的小节中已经给出了大部分场景需求的解决方案,但仍是很难涵盖全部的可能性。 因此你本身须要有能力去分析遇到的问题,而后去寻找对应的解决方案,你能够按照如下思路去分析和解决问题:
babel-loader
。在解决问题的过程当中有如下2点能力很重要:
最重要的是你须要多实战,本身去解决问题,这有利于加深你的影响和理解,而不是只看不作。