长期缓存

为了有效的缓存文件,URL需要有一个hash或版本号,你可以手动的将输出文件移到一个叫v1.3的文件夹
但是这有几个缺点:额外的开发工作并且没有任何改变的文件不能存缓存中获取.
Webpack可以在文件名之后添加hash串,加载器(worker-loader, file-loader)输出的文件已经
做了这项工作,需要你允许块文件使用它,这里有两个级别

  • 计算所有块文件的hash并且添加它
  • 计算每个块文件的hash并且添加它

方式一:对打包文件增加一个hash串

命令行方式webpack ./entry output.[hash].bundle.js,配置文件方式:

1
2
3
4
5
6
7
8
{
output: {
path: path.join(__dirname, "assets", "[hash]"),
publicPath: "assets/[hash]/",
filename: "output.[hash].bundle.js",
chunkFilename: "[id].[hash].bundle.js"
}
}

方式二:每一个块文件一个hash串
可以通过添加[chunkhash]到配置文件的filename属性中
--output-chunk-file [chunkhash].js

1
output: { chunkFilename: "[chunkhash].bundle.js" }

注意:你需要在HTML中引入带有hash串的入口块,你需要从stats中提取出hash串。
为了兼容代码热部署,你需要使用方式一,但是不要在publicPath中使用hash串。

从stats中获取文件名

你可能需要访问最终的文件以便在HTML中嵌入,这段资料可获得webpack的stats,如果你使用命令
行,可通过--with获得json的标准输出。
可以在配置文件中添加插件比如assets-webpack-plugin来访问stats对象,这是一个将stats
写入文件的例子:

1
2
3
4
5
6
7
8
9
plugins: [
function() {
this.plugin("done", function(stats) {
require("fs").writeFileSync(
path.join(__dirname, "..", "stats.json"),
JSON.stringify(stats.toJson()));
});
}
]

stats的JSON格式包含了一个有用的属性assetsByChunkNam,它是一个对象,以块名字为键,对应
的文件名为值。
注意:如果一个块文件输出了多个资源文件,那么stats是数组,数组的第一个元素是你的
javascript资源。

桌面环境集成

对于桌面应用,不同的操作系统在左面环境中提供了不同的特性。比如在windows下应用程序可以将
应用程序的图标放在任务栏中的跳转列表上,在Mac上,应用可以将自定义菜单放在dock菜单上。
这篇指南将会说明如何利用Electron APIS集成你的应用到桌面环境上。

Notifications(通知)(Windows, Linux, Mac)

这三个操作系统都为用户提供了应用发送通知的方法。Electron允许开发者通过
HTML5 Notification API很方便的发送通知.
用操作系统原生的通知APIs去显示.
注意:由于这是HTML5的API所以只能在渲染进程中使用。

1
2
3
4
5
6
7
let myNotification = new Notification('Title', {
body: 'Lorem Ipsum Dolor Sit Amet'
});

myNotification.onclick = () => {
console.log('Notification clicked');
};

尽管代码和用户体验在不同的平台上基本相同,但还是有区别的.

Windows

  • 在windows10, 通知可以工作。
  • 在windows8.1,8上,需要安装
    Application User Model ID.aspx)
    到开始屏幕上,这不是将你的应用固定到开始屏幕。
    Windows7及以下,不支持通知,可以通过
    Tray API
    发送一个气泡通知。
    此外,通知的最大长度为250个字符,Windows团队建议通知的长度保持在200个字符。

Linux ToDo

macOS

通知在macOS上是直接转发的,你应该需要了解Apple's Human Interface guidelines regarding notifications.
注意,通知被限制在256bytes,如果超出将会被截断。

最近文档(Windows和macOS)

Windows 和 macOS 提供获取最近文档列表的便捷方式,那就是打开跳转列表或者鱼眼菜单。
增加一个文件到最近的访问文档,使用app.addRecentDocumentAPI.

1
app.addRecentDocument('/Users/USERNAME/Desktop/work.type');

使用app.clearRecentDocuments清空最近访问列表。

1
app.clearRecentDocuments();

Windows注意点

为了可以在windows上使用这个特性,你的应用需要被注册成为一种文件类型的句柄,否则,在你
注册之前,文件不会出现在跳转列表,你可以在ApplicationRegistration.aspx)
注册你的应用,并了解相关事宜.

macOS注意点

当在最近打开文档中访问该文件时,app模块的’open-file’事件将会被发出。

自定义菜单(macOS)

macOS 可以让开发者定制自己的菜单,通常会包含一些常用特性的快捷方式。
使用app.dock.setMenuAPI来设置你自定义的菜单,仅在macOS上可行。

1
2
3
4
5
6
7
8
9
10
11
12
13
const electron = require('electron');
const app = electron.app;
const Menu = electron.Menu;

const dockMenu = Menu.buildFromTemplate([
{ label: 'New Window', click() { console.log('New Window'); } },
{ label: 'New Window with Settings', submenu: [
{ label: 'Basic' },
{ label: 'Pro'}
]},
{ label: 'New Command...'}
]);
app.dock.setMenu(dockMenu);

用户任务(Windows)

在 Windows,你可以特别定义跳转列表的 Tasks 目录的行为,引用 MSDN 的文档:
Applications define tasks based on both the program’s features and the key things a user is expected to do with them. Tasks should be context-free, in that the application does not need to be running for them to work. They should also be the statistically most common actions that a normal user would perform in an application, such as compose an email message or open the calendar in a mail program, create a new document in a word processor, launch an application in a certain mode, or launch one of its subcommands. An application should not clutter the menu with advanced features that standard users won’t need or one-time actions such as registration. Do not use tasks for promotional items such as upgrades or special offers.
It is strongly recommended that the task list be static. It should remain the same regardless of the state or status of the application. While it is possible to vary the list dynamically, you should consider that this could confuse the user who does not expect that portion of the destination list to change.

不同于macOS的菜单,windows上的用户任务的工作方式就像快捷方式一样,当用户点击了一个任务,
一个程序将会被传入特定的参数并且运行.
在你的应用中使用app.setUserTasksAPI设置用户任务

1
2
3
4
5
6
7
8
9
10
app.setUserTasks([
{
program: process.execPath,
arguments: '--new-window',
iconPath: process.execPath,
iconIndex: 0,
title: 'New Window',
description: 'Create a new window'
}
]);

调用app.setUserTasks并传入空数组来清空任务列表

1
app.setUserTasks([]);

当你的应用关闭时,用户任务会仍然会出现,在你的应用被卸载前,任务指定的图标和程序的路径
必须是存在的。

缩略图工具栏

在 Windows,你可以在任务栏上添加一个按钮来当作应用的缩略图工具栏。它将提供用户一种用户
访问常用窗口的方式,并且不需要恢复或者激活窗口。
使用BrowserWindow.setThumbarButtons设置应用程序的缩略工具栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const {BrowserWindow} = require('electron');
const path = require('path');

let win = new BrowserWindow({
width: 800,
height: 600
});

win.setThumbarButtons([
{
tooltip: 'button1',
icon: path.join(__dirname, 'button1.png'),
click() { console.log('button1 clicked'); }
},
{
tooltip: 'button2',
icon: path.join(__dirname, 'button2.png'),
flags: ['enabled', 'dismissonclick'],
click() { console.log('button2 clicked.'); }
}
]);

调用BrowserWindow.setThumbarButtons并传入空数组即可清空缩略图工具栏:

1
win.setThumbarButtons([]);

Unity Launcher Shortcuts (Linux) TODO
Progress Bar in Taskbar (Windows, macOS, Unity) TODO

优化

压缩

通过简单的配置可以让webpack压缩你的javascript代码。

1
--optimize-minimize 或new webpack.optimize.UglifyJsPlugin()

这是一个简单并有效优化你应用的方式
如果你已经知道webpack给每个模块和分块提供了id去标识他们,webpack可以通过一个简单的配置
改变id的分配,通过给常用模块最短的id进行优化

1
--optimize-dedupe 或new webpack.optimize.DedupePlugin()

入口块有更高的文件大小优先级

去重

如果使用一些库时,会发现一些文件是相同的,webpack可以找到这些文件并且去重,这可以阻止
在打包文件中包含重复的代码,并且在运行时提供一个复制函数,这不会影响语法问题,可以这样
嵌入它

1
--optimize-dedupe 或 new webpack.optimize.DedupePlugin()

这个功能会在入口快之前添加。

分块

当在写代码时,可能你已经添加了一下代码分离点来按需加载,在编译之后你可能会发现有太多特别
小的分块,Http请求负担加重,幸运的是,你可以后置的去处理这些块文件,可以有两种配置。

  • 限制块文件的最大个数:

    1
    --optimize-max-chunks 15或者new webpack.optimize.LimitChunkCountPlugin({maxChunks: 15})
  • 限制最小块文件的大小

    1
    --optimize-min-chunk-size 10000或new webpack.optimize.MinChunkSizePlugin({minChunkSize: 10000})

webpack会小心的处理分块合并(优先合并有重复的分块),任何东西都不会合并到入口块中,所以
不会影响首页加载时间

单页面应用

webpack就是用来处理单页面应用而设计和优化的。
你或许已经分离应用到多个分块,根据不同的路由加载不同的块,入口块只包含了路由和一些库,但
没有实质内容,这种做法对于应用中的浏览切换时很好的,但是首页加载需要两次请求:一次是路由
一次是页面内容。
如果你使用了 HTML5 的 history api 来做页面内容和 url 的一一对应,那么服务端就能根据
url来知道请求的具体单页应用中的哪一个页面.这样,可以通过服务端直接同时输出请求页面的处理分块,
利用浏览器的并行加载来节省网络请求时间.

1
2
<script src="entry-chunk.js" type="text/javascript" charset="utf-8"></script>
<script src="3.chunk.js" type="text/javascript" charset="utf-8"></script>

你可以从stats中提取出分块的名字(stats-webpack-plugin导出构建的stats)

多页面应用

当你编译多页面应用时,你想在页面之间共享公共的代码,这对于webpack很简单,只需要使用多个
入口点``

1
2
3
4
5
6
7
8
9
10
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
p3: "./page3"
},
output: {
filename: "[name].entry.chunk.js"
}
}

这会生成多个入口文件,p1.entry.chunk.js,p2.entry.chunk.js,p3.entry.chunk.js
但是他们可以共享额外的块文件。
如果入口块有使用相同的模块,CommonsChunkPlugin可以找出相同的模块然后把他们提取出来
放到一个公共分块中,这样,页面中只需要引入两个 script,一个是公共分块另一个则是该页面的入口块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
p3: "./page3"
},
output: {
filename: "[name].entry.chunk.js"
},
plugins: [
new CommonsChunkPlugin("commons.chunk.js")
]
}

这样会生成p1.entry.chunk.jsp2.entry.chunk.jsp3.entry.chunk.js 以及
commons.chunk.js。首先加载 commons.chunk.js,然后加载对应的入口块。
你可以通过选择入口块生成多个公共块,公共块也可以嵌套使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
p3: "./page3",
ap1: "./admin/page1",
ap2: "./admin/page2"
},
output: {
filename: "[name].js"
},
plugins: [
new CommonsChunkPlugin("admin-commons.js", ["ap1", "ap2"]),
new CommonsChunkPlugin("commons.js", ["p1", "p2", "admin-commons.js"])
]
};
// <script>s required:
// page1.html: commons.js, p1.js
// page2.html: commons.js, p2.js
// page3.html: p3.js
// admin-page1.html: commons.js, admin-commons.js, ap1.js
// admin-page2.html: commons.js, admin-commons.js, ap2.js

高级用法,公共块的代码也可以直接运行

1
2
3
4
5
6
7
8
9
10
11
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
p1: "./page1",
p2: "./page2",
commons: "./entry-for-the-commons-chunk"
},
plugins: [
new CommonsChunkPlugin("commons", "commons.js")
]
};

查看multiple-entry-points example
advanced multiple-commons-chunks example.

快速开始

快速开始

Electron可以使用纯粹的javaScript通过调用原生的API创建桌面应用,可以把它当做是nodejs
运行时多变多样的专注于桌面应用而不是web服务器。
这并不意味着Electron是绑定了GUI库的javaScript,Electron使用Web页面作为它的UI,你可以
把它当作是简易版的Chrom浏览器,由javascript控制。

主线程

在Electron中,运行package.jsonmain脚本的进程是主进程,在主进程运行的脚本通过创
建Web页面来展示GUI.

渲染进程

由于Electron使用Chromium来展示web页面,所以Chromium的多进程架构也被利用上了,每一个
在Electron的web页面都运行着自己的进程,这样的进程称之为渲染进程。
在一般的浏览器中,web页面通常运行在沙河环境下,这样是不能访问原生资源的,然而,Electron
用户,有权利在web页面中调用nodejs API,可以与底层操作系统交互.

主进程与渲染进程的不同

主进程通过创建BrowserWindow实例来创建web页面,每个BrowserWindow实例在它自己的渲染
进程中运行web页面,当一个BrowserWindow实例被销毁后,相应的渲染进程也终止了。
主进程管理所有的web页面以及他们相应的渲染进程,每个渲染进程是相互隔离的并且只关心运行在
自己上的web页面。
在web页面中,调用原生GUI相关的API是不允许的,因为在页面中管理原生的GUI资源是非常危险的
并且容易泄露资源,如果你想在web页面中执行GUI的的操作,页面的渲染进程必须与主进程通讯去
请求主进程执行这些操作.
在Electron中,渲染进程与主进程的通讯有多种方式,比如ipcRenderipcMain模块去发送
消息,并且remote模块用于RPC方式通讯,这是一个常见的问题,如何在web页面中共享数据

完成第一个Electron应用

通常,一个Electron应用的结构类似下面的方式

1
2
3
4
your-app/
├── package.json
├── main.js
└── index.html

package.json的格式和node的模式是一样的,通过main字段指定的脚本文件就是你的应用
程序的启动脚本,它将会运行在主进程中,package.json通常是下面的格式

1
2
3
4
5
{
"name" : "your-app",
"version" : "0.1.0",
"main" : "main.js"
}

注意:如果在package.json中没有声明main字段,Electron会尝试加载index.js
main.js应该用于创建窗口并且处理系统事件,典型的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const electron = require('electron');
// Module to control application life.
const {app} = electron;
// Module to create native browser window.
const {BrowserWindow} = electron;

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;

function createWindow() {
// Create the browser window.
win = new BrowserWindow({width: 800, height: 600});

// and load the index.html of the app.
win.loadURL(`file://${__dirname}/index.html`);

// Open the DevTools.
win.webContents.openDevTools();

// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
});
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);

// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});

app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

最后,你想展示的index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
We are using node <script>document.write(process.versions.node)</script>,
Chrome <script>document.write(process.versions.chrome)</script>,
and Electron <script>document.write(process.versions.electron)</script>.
</body>
</html>

运行你的应用

一旦你已经创建了最初的main.jsindex.html,package.json,你可能会想尝试在本地
运行并测试,看看是不是和期望的那样正常运行。
electron-prebuilt
electron-prebuilt是一个npm模块它包含了所使用的Electron预编译版本,如果你已经全局
安装了它,你只需要在你的应用程序目录下执行

1
electron .

如果你是局部安装的,运行

1
./node_modules/.bin/electron .

样式脚本

嵌入的样式脚本

通过使用style-loadercss-loader,可以webpack打包的javascript文件中嵌入样式脚本.
通过这种方式可以是css模块化,这种方式使用很简单require("./stylesheet.css")

通过npm安装

1
npm install style-loader css-loader --save-dev

配置

通过一下的方式支持require(xxx.css)这种方式

1
2
3
4
5
6
7
8
{
// ...
module: {
loaders: [
{ test: /\.css$/, loader: "style-loader!css-loader" }
]
}
}

对于css预处理语言,可以参照加载器的配置示例,你可以链式的使用它。
记住,掌握加载器的执行顺序很难,所以设计好样式让其与顺序无关(但是css文件内的样式顺序
还是得依靠的)。
使用css

1
2
3
// in your modules just require the stylesheet
// This has the side effect that a <style>-tag is added to the DOM.
require("./stylesheet.css");

分离css打包文件

extract-text-webpack-plugin结合使用,可以生成一个原生的css文件。
使用两种方式实现代码分离

  • 每个初始块创建一个css文件,然后在其余块中引入(推荐)
  • 每个初始块创建一个 css 文件,其中同时包含了依赖块中的样式
    推荐第一种方式是因为可以减少页面初始化时间,在小型多入口项目中第二种方式可以利用缓存来减少
    http消耗

安装extract-text-webpack-plugin

1
npm install extract-text-webpack-plugin --save

通用

使用这个插件需要标记加载器
把css从初始化块中分离到css文件中
这个例子适用于多入口以及单页面应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// webpack.config.js
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
// The standard entry point and output config
entry: {
posts: "./posts",
post: "./post",
about: "./about"
},
output: {
filename: "[name].js",
chunkFilename: "[id].js"
},
module: {
loaders: [
// Extract css files
{
test: /\.css$/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader")
},
// Optionally extract less files
// or any other compile-to-css language
{
test: /\.less$/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader")
}
// You could also use other loaders the same way. I. e. the autoprefixer-loader
]
},
// Use the plugin to specify the resulting filename (and add needed behavior to the compiler)
plugins: [
new ExtractTextPlugin("[name].css")
]
}

你将获得以下文件

  • posts.js posts.css
  • post.js post.css
  • about.js about.css

把所有的样式分离到css文件中

使用第二种方式只需要设置allChunkstrue

1
2
3
4
5
6
7
8
9
// ...
module.exports = {
// ...
plugins: [
new ExtractTextPlugin("style.css", {
allChunks: true
})
]
}

你将会得到以下输出文件

  • posts.js
  • post.js
  • about.js
  • style.css

公共块的样式

你可以与CommonsChunkPlugin结合使用分离出一个css文件,在这个示例中公共快也会生成一个
css文件

1
2
3
4
5
6
7
8
// ...
module.exports = {
// ...
plugins: [
new webpack.optimize.CommonsChunkPlugin("commons", "commons.js"),
new ExtractTextPlugin("[name].css")
]
}

你会获得以下输出文件

  • commons.js commons.css
  • posts.js posts.css
  • post.js post.css
  • about.js about.css

支持的平台

支持的平台

以下的平台是Electron支持的

  • macOS
    只支持macOS64进制的系统,支持的最低版本是macOS 10.9
  • Windows

    支持windows7以后的系统,老系统暂不支持,并且不会运行,32位和64位都支持,ARM版本的windows
    暂不支持

  • Linux

    预编译的Electronia32(i686)x64(amd64)二进制文件都是在Ubuntu12.04上编译的。
    一下的linux系统证实了可以运行预编译版本的Electron

    • Ubuntu 12.04 and later
    • Fedora 21
    • Debian 8

Electron-常见问题

Electron框架让你通过CSS、JS、HTML写跨平台的桌面应用,它基于Node.js以及Chrom内核,Atom
编辑器就是通过它完成的。

FAQ(Frequently Asked Questions)常见问题

什么时候=会升级到最新版本的Chrome
通常会在稳定版本的Chrom发布1-2周内,更新Electron内的Chrome版本,Electron只会使用稳定
版本的Chrome,但如果在beta或dev有重要的更新的话,会使用补丁的方式应用到现版本的Chrome上
什么时候会升级到最新版本的nodejs
当一个新版本的nodejs发布之后,通常会在1月之后更新Electron,这样可以避免引入新版本的nodejs
所带来的bug,这太常见了.
Nodejs的新特性通常是在V8带来的,由于Electron使用的是Chrome装配的V8,所以Electron内已经
有了新版本Nodejs的新特性.
如何在不同的页面共享数据
在不同的网页之间共享数据最简单的方法就是使用浏览器中以实现的HTML5Api,比较好的方案就是
Storage API, localStorage, sessionStorage, 还有IndexDB。或者使用Electron的
IPC系统,将对象储存在主进程中的一个全局变量中,然后在所有的渲染进程中通过Electron
remote属性访问它。

1
2
3
4
// In the main process.
global.sharedObject = {
someProperty: 'default value'
};

1
2
// In page 1.
require('electron').remote.getGlobal('sharedObject').someProperty = 'new value';
1
2
// In page 2.
console.log(require('electron').remote.getGlobal('sharedObject').someProperty);

应用的窗口/托盘在一段时间之后不见了
这通常是因为储存窗口/托盘的变量被垃圾收集器收集了。
如果遇到这个问题,下面的文章会有所帮助

改变为

1
2
3
4
let tray = null;
app.on('ready', () => {
tray = new Tray('/path/to/icon.png');
});

为什么不能在Electron中使用jQuery、RequireJs、Meteor、AngularJs
由于nodejs集成了Electron,一些特殊的符号(标识)比如require,exports,’module’会被
插入到DOM中,这会导致一些类库插入相同命名的标识失败。

1
2
3
4
5
6
// In the main process.
let win = new BrowserWindow({
webPreferences: {
nodeIntegration: false
}
});

如果想继续使用Nodejs和Electron提供的API,你需要在页面中重命名这些符号(标识)在引入其
它类库时

1
2
3
4
5
6
7
8
9
<head>
<script>
window.nodeRequire = require;
delete window.require;
delete window.exports;
delete window.module;
</script>
<script type="text/javascript" src="jquery.js"></script>
</head>

require(electron).xxx未定义
在使用electron的内置模块时,可能会遇到这样的问题

1
2
require('electron').webFrame.setZoomFactor(1.0);
Uncaught TypeError: Cannot read property 'setZoomLevel' of undefined

这是因为你通过npm安装了全局的或本地的electron模块,它覆写了Electron的内置模块
你可以打印出electron的路径来核实你是否使用了正确的内置模块

1
console.log(require.resolve('electron'));

检查一下是否符合下面的形式

1
"/path/to/Electron.app/Contents/Resources/atom.asar/renderer/api/lib/exports/electron.js"

如果是node_modules/electron/index.js这种格式的话,你需要删除或者重命名npm安装的electron模块

1
2
npm uninstall electron
npm uninstall -g electron

如果依然遇到同样的问题,很可能是因为你在错误的进程中使用了此模块,比如electron.app只能
在主进程中使用,而electron.webFrame只能在渲染进程中使用.

代码拆分

对于大型项目,将所有的代码放在一个文件中不是很好,特别是一些代码只会在响应的情况下才会
用到,webpack的其中一个特性就是将你的原代码拆分成块按需加载,一些其余的打包工具将它称之
为”层”,”片段”,webpack将其称为”代码拆分”.
这是一个可被选择的特性,你可以在你的原代码中定义拆分点,webpack会维持好依赖性,输出文件
以及运行时文件。
为了澄清一个常见的误解,代码拆分并不仅仅是将公共代码提取出来到一个公共的模块,更重要的特性
是可以将拆出来的代码块按需加载,这可以保证首次加载的代码更少,请求会按需加载所需要的代码。

定义分离点

AMD和CommonJs指定不同的方法实现按需加载,webpack都支持并且并且都是定义为分离点的形式。
CommonJs: require.ensure

1
require.ensure(dependencies, callback)

require.ensure方法保证 每个依赖都会在回调函数被同步的请求进来,回调函数被执行时,
require是回调函数的参数.例如:

1
2
3
4
require.ensure(["module-a", "module-b"], function(require) {
var a = require("module-a");
// ...
});

注意:require.ensure仅仅是加载模块,但是并不会去执行他们

AMD:require

AMD规范以下面的方式定义了一个异步的require方法

1
require(dependencies, callback)

当调用时,所有的依赖都会被加载,并且将他们作为参数给回调函数,例如

1
2
3
require(["module-a", "module-b"], function(a, b) {
// ...
});

注意:AMDrequire加载并执行响应的模块,在webpack中模块是从左向右执行的,并且可以省略
回调函数。

ES6模块

Webpack不支持ES6模块,直接使用require.ensure或者require取决于设计哪个模块组成你的
编译.
Webpack1.x.x(2.0即将来临)原生的不支持ES6模块,但是你可以通过转换器解决他,比如使用Babel
转换ES6的import语法为CommonJs或者AMD模块,这种途径是有效的,但是当动态加载时会有警告。
这种模块添加方式import x from 'foo'是故意设计为静态分析的,意味着你不能用于动态导入

1
2
// INVALID!!!!!!!!!
['lodash', 'backbone'].forEach(name => import name )

幸运的是,JavaScript有loader规范去处理动态导入:System.load或者System.import)
原生的API和require的定义是等价的,但是大多数编译器不支持转化System.load为’require.ensure’
所以如果想使用动态代码拆分的话直接使用require.ensure

1
2
3
4
5
6
7
//static imports
import _ from 'lodash'

// dynamic imports
require.ensure([], function(require) {
let contacts = require('./contacts')
})

分块内容

分离点处的所有依赖都会被独立成一个块,依赖会递归的被添加。
如果在分离点处的回调函数是一个函数,webpack 会把该函数里的依赖都提取到按需加载的代码块中。

分块优化

如果两个分块包含相同的模块,它们会被合并成一个,这会造成分块有多个父模块,如果一个模块
在一个分块中的所有父模块都可用,那么它将会从该分块中移除。
如果一个分块包含了另一个分块中的所有模块,它会被储存起来,满足多个分块.

分块加载

根据配置文件的target选项的值,打包时执行不同的逻辑去加载分块,比如webtarget,分块通
过jsonp被加载,一个分块只会被加载一次,并发的请求将会被合并成一个,运行时检查加载的分块是
否满足多个分块

分块类型

入口块

AMD

AMD(Asynchronous Module Definition异步模块定义),因为CommonJs模块系统不适用于浏览器,
因为CommonJs是同步的

规范

使用ddefine定义模块

define

AMD定义模块通过define函数,它是一个有参数的函数

1
define(id?: String, dependencies?: String[], factory: Function|Object);

id
定义模块的名称,可选择的
dependencies
指定定义该模块需要依赖其余的哪些模块,是一个模块标识的数组,缺省时默认值是 [“require”, “exports”, “module”].
factory
最后的这个参数就是谁定义了这个模块,它可以是一个对象也可以是一个函数(只调用一次),如果是函数
的话,函数的返回值就是模块暴露出去的值。

例子

定义一个叫myModule并且依赖jQuery的模块

1
2
3
4
5
6
define('myModule', ['jquery'], function($) {
// $ is the export of the jquery module.
$('body').text('hello world');
});
// and use it
require(['myModule'], function(myModule) {});

在webpack中一个命名模块仅局部可用,而在require.js中则全局可用

匿名模块

定义一个没有明确id的模块

1
2
3
define(['jquery'], function($) {
$('body').text('hello world');
});

多个依赖

注意每个模块的输出值将会传到factory函数中

1
2
3
4
define(['jquery', './math.js'], function($, math) {
// $ and math are the exports of the jquery module.
$('body').text('hello world');
});

输出值

定义一个输出本身的模块

1
2
3
4
5
6
7
8
define(['jquery'], function($) {

var HelloWorldize = function(selector){
$(selector).text('hello world');
};

return HelloWorldize;
});

使用require加载模块

1
2
3
4
define(function(require) {
var $ = require('jquery');
$('body').text('hello world');
});

CommonJs

CommonJs组织定义了一种模块格式,通过确保每一个模块都运行在它自己的命名空间中来解决
JavaScript的作用域问题。
通过强制模块明确的输出它想暴露出去的变量,并且引用其它模块的方式来完成。
为了实现CommonJs,它给了两个工具.

  1. require()函数,在当前作用域引用别的模块.
  2. module对象,允许从当前作用域输出变量。

简单的JavaScript

下面是一个未使用CommonJs的例子,定义一个status.js脚本文件,该文件只包含一个值,它将会
在别的脚本文件中使用。

1
2
// salute.js
var MySalute = "Hello";

在第二个名叫world.js的脚本文件中使用在status.js中定义的变量

1
2
// world.js
var Result = MySalute + " world!";

模块定义

当然world.js无法工作,因为MySalute未定义,我们需要定义每一个文件为一个模块。

1
2
3
// salute.js
var MySalute = "Hello";
module.exports = MySalute;
1
2
3
// world.js
var Result = MySalute + "world!";
module.exports = Result;

在这里使用特殊的对象module并且将变量的引用赋值给module.exports,CommonJs模块系统将
会知道这个对象是想暴露给全局。salute.js暴露MySalute, world.js暴露Result

模块依赖

缺省异步定义依赖,我们定义了每个javascript文件为一个单独的模块,但是world.js依旧需要
知道谁定义了MySalute

1
2
3
// salute.js
var MySalute = "Hello";
module.exports = MySalute;
1
2
3
4
// world.js
var MySalute = require("./salute");
var Result = MySalute + "world!";
module.exports = Result;

函数例子

1
2
3
4
// moduleA.js
module.exports = function( value ){
return value*2;
}

1
2
3
// moduleB.js
var multiplyBy2 = require('./moduleA');
var result = multiplyBy2( 4 );