"WMR" 是什么意思?
Heh - yeah WMR was originally a bad pun: "Warm Module Replacement".
html/js 中具有发起 http 请求的方式早就了 Native Browser 的可能性
<script>
标签<link>
标签- ES6
import
语句 - css 中的
@import
语句
import 语句在一个服务中,能自己发起 http 请求, 例如下面的 html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>WMR Demo</title>
<meta name="description" content="WMR Demo" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="icon" href="data:" />
<link rel="modulepreload" href="/index.tsx" crossorigin />
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<script type="module" src="/index.tsx"></script>
</body>
</html>
以上的 link/script
标签发起 http
请求之后,响应的内容是由服务端确定实际内容, 对于 index.tsx 服务端响应的结果, 如果是浏览器能够直接运行的 js/资源
代码文件, 然后内部使用 import/import() 语句的实现代码导入, 那么就实现了 native browswer
,管理依赖.但是它抛出的新的问题就是,如果是直接导入 包名
, 这种情况就需要单独处理.
WMR 功能特点
WMR 是一个 Preact 的 esm 的开发服务器,支持下面的主要功能:
特点 | 说明 |
---|---|
即时启动 | 服务启动随同启动 |
HOT热重载 | 模块热替换 |
无需配置 | 以 html 为入口的 Preact 项目 |
TypeScript 支持 | TypeScript 类型支持 |
生产类型进行优化 | 支持预渲染 |
rollup 插件兼容 | 与 rollup 生态进行兼容 |
实现 wmr 大致需要的工具与服务:
- http 服务 (提供 http 服务)
- websocket 服务 (前端通信)
- 文件监听变化服务 (chokidar)
- 代码转换工具(模拟 rollup -> NoRollup 兼容 rollup 插件)
- 打包服务(一般用于生产优化,这里适用 rollup)
wmr 依赖列表
基础 | 说明 |
---|---|
polka | 类 express Node.js 服务端框架,简单应用比 Express 快 33-50% |
sirv | 经过优化的轻量级中间件,用于处理对静态资产的请求 |
sade | 命令行工具程序 |
ws | web socket 工具 |
rollup | 下一代的 esm 的打包器 |
chokidar | 文件监视器 |
jest | javascript 测试运行器 |
... |
使用 wmr 创建一个 Preact 项目
npm init wmr your-project-name
yarn create wmr your-project-name
目录结果如下:
理解 wmr 简单设计流程
cli 从 sada 开始
从 sada 命令行开始(命令行接口的本质,就是将 程序中需要的数据例如 options,映射到到程序内部)
import sade from 'sade';
prog
.command('start', 'Start a development server', { default: true })
.option('--public', 'Your web app root directory (default: ./public)')
.option('--port, -p', 'HTTP port to listen on (default: $PORT or 8080)')
.option('--host', 'HTTP host to listen on (default: localhost)')
.option('--http2', 'Use HTTP/2 (default: false)')
.option('--compress', 'Enable compression (default: enabled)')
.option('--profile', 'Generate build statistics')
.option('--reload', 'Switch off hmr and reload on file saves')
.action(opts => {
opts.optimize = !/false|0/.test(opts.compress);
opts.compress = bool(opts.compress);
if (/true/.test(process.env.PROFILE || '')) opts.profile = true;
run(start(opts));
});
prog.parse(process.argv);
主要服务和文件变化
如何上的 options 通过处理,传递给了 server 函数,启动一个 polka 服务(并且监听文件变化):
const cloned = deepCloneJSON(options);
let instance = await bootServer(cloned, configWatchFiles);
const watcher = watch(configWatchFiles, {
cwd: cloned.cwd,
disableGlobbing: true
});
options 深度 clone 之后,传递给了 bootServer 函数, bootServer 函数中真正的实现了端口的监听:
app.listen(options.port, options.host, () => {
const addresses = getServerAddresses(app.server.address(), {
host: options.host,
https: app.http2
});
const message = `dev server running at:`;
process.stdout.write(formatBootMessage(message, addresses));
const port = +app.server.address().port;
resolveActualPort(port);
});
但是在启动监听服务之前,需要一个创建关闭,这里就需要印出关闭的内容:
- httpServer
- chokidar文件监听
- websocket
function makeCloseable(server) {
/** @type {Set<import('net').Socket>} */
const sockets = new Set();
let listened = false;
server.on('connection', s => {
sockets.add(s);
s.on('close', () => sockets.delete(s));
});
server.once('listening', () => (listened = true));
return async () => {
sockets.forEach(s => s.destroy());
if (!listened) return;
await new Promise((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()));
});
};
}
关闭时,需要关闭已经链接过的 socket 的接口.
核心中间件 options
options.middleware = [].concat(
// @ts-ignore-next
options.middleware || [],
wmrMiddleware({
...options,
onError: sendError,
onChange: sendChanges
}),
injectWmrMiddleware(options)
);
wmrMiddleware 中间件
代码转换
用于支持 typescript/less/sass/... 等主流语言等. 转换过程是一个非常复杂的过程,因为对 Native 的支持,需要考虑内容非常多:
- typescript 支持(语言支持)
- 模块的加载(commonjs 转转成 esm)
- ...
wmr 以 rollup 为核心实现了 rollup-plugin-container 的 NoRollup 的容器, rollup-plugin-container 中实现了 rollup 的插件钩子函数
以及钩子函数的上下文
. 所以要理解 rollup 以及插件体系
理解要透彻
.
injectWmrMiddleware
将 html 代码中注入 wmr 所需要的js代码: websocket 和模块热替换等功能.
const transformInjectWmr = async tree => {
tree.walk(node => {
if (node.tag === 'head') {
node.content.unshift('\n\t\t', {
tag: 'script',
attrs: { type: 'module' },
content: ["\n\t\t\timport '/_wmr.js';\n\t\t"]
});
}
return node;
});
};
server
server.js 是 app 的实现最底层, 实现如下
export default async function server({ cwd, root, overlayDir, middleware, http2, compress = true, optimize, alias }) {
const app = polka({/**/})
// 挂载 websocket
app.ws = new WebSocketServer(app.server, '/_hmr');
// 处理 @npm 的模块
app.use('/@npm', npmMiddleware({ alias, optimize, cwd }));
// ...
return
}
这里需要说明, wmr 需要将 node_module 中依赖包准换成以 /@npm 开头的包放入.cache/@npm/
:
npm 依赖中间件 npmMiddleware
export default function npmMiddleware({ source = 'npm', alias, optimize, cwd } = {}) {
const code = await bundleNpmModule(mod, { source, alias, cwd });
}
rollup bundleNpmModule
bundleNpmModule 中 rollup 进行打包(why? 因为 rollup 中的可能也有打包)
const bundle = await rollup.rollup({
input: mod,
onwarn: onWarn,
cache: npmCache,
shimMissingExports: true,
treeshake: false,
preserveEntrySignatures: 'allow-extension',
plugins: [
// ...
npmProviderPlugin,
// ...
commonjs({
extensions: ['.js', '.cjs', ''],
sourceMap: false,
transformMixedEsModules: true
}),
// ...
]
});
小结
- 命令行收集 options 等数据给程序,发生数据的映射过程开始
- import/import() 的 native browser
- http 服务, websocket 服务,文件监听,中间件处理请求 NoRollup 模拟rullup转换打包过程
- rollup 打包与插件, 实现优化
- 前端工程化,要求对前端生态熟悉
- 开始关注 Preact 生态,因为还有类似的 fresh 轻量级框架开始产生
参考
- wmr 官方网站 wmr.dev/
- wmr 代码仓库 github.com/preactjs/wm…
- Origin/meaning of "WMR"? github.com/preactjs/wm…
- sirv npm 包 www.npmjs.com/package/sir…
- polka仓库 github.com/lukeed/polk…
- sade 命令行接口 github.com/lukeed/sade…
- websockets/ws github.com/websockets/…
- rollup rollupjs.org/
- fresh - The next-gen web framework.fresh.deno.dev/