@FunC
2018-01-03T00:34:19.000000Z
字数 7663
阅读 1952
Node.js
尽管 Node.js 和最流行的浏览器—Chrome 一样,都使用了 V8 引擎,然而想要让代码能够共享还是需要花费一定的功夫。例如 Node.js 中没有 DOM, 而浏览器端没有读写文件的能力。同时 Node.js 已经支持大部分 ES2015 的新特性,而浏览器方面则推进缓慢。
浏览器端对模块的支持/实现是非常零散的,可能有下面几种情况:
* 没有模块系统,依靠全局对象访问不同模块
* 有基于 AMD(异步模块定义)的环境
* 有将 CommonJS 模块系统抽象出来的环境
而使用 UMD,我们能将代码从模块系统中抽象出来,直接在环境中使用。
观察以下代码:
umdModule.js
(function(root, factory) {
// 检测是否存在 AMD 环境
if (typeof define === 'function' && define.amd) {
define(['mustache'], factory);
// 检测 CommonJS 环境
} else if (typeof module === 'object' && typeof module.exports === 'object') {
var mustache = require('mustache');
module.exports = factory(mustache);
// 使用全局对象
} else {
root.UmdModule = factory(root.Mustache);
}
// 采用依赖注入的方式
} (this, function(mustache) {
var template = '<h1>Hello <i>{{name}}</i></h1>';
mustache.parse(template);
return {
sayHello: function(toWhom) {
return mustache.render(template, {name: toWhom});
}
};
}));
上述代码定义了一个模块,它依赖于模块 mustache
,我们通过依赖注入的方式引入。
1. 首先,利用 typeof 操作符检测是否存在 define
函数和 define.amd
标志,以判断是否存在 AMD 环境。
2. 若不存在 AMD 环境,检测是否存在 module
和 module.exports
,以判断是否存在 CommonJS 模块环境。
3. 若依然没有,则使用全局对象。要求依赖的名称必须为Mustache
4. 传入 this
作为全局对象(浏览器下为 window
),以及一个工厂函数,触发自执行函数。
因为 UMD 模式需要书写大量样板代码(boilerplate),导致难以在每个环境中进行测试。因此,UMD 模式实际上只适合用于包裹已经开发完整且经过测试的模块,而不是从头开发。我们应该使用 Webpack 等工具帮助我们自动化完成。
此外,AMD,CommonJS 和 浏览器的全局对象这三者不等同于全部的模块系统(例如 ES2015 的模块规范),上述的 UMD 样板代码并不能覆盖全部的模块系统。
ES2015 标准的其中一个特性,就是引入了内置的模块系统。然而至今仍未完全实现,此处仅作简单介绍。
ES2015 模块的目标是汲取 CommonJS 和 AMD 模块的优点:
* 像 CommonJS 一样支持单点导出(single exports)和循环依赖
* 像 AMD 一样支持异步加载和可配置模块加载
* 此外,得益于声明式语法,支持静态分析和优化,从而实现按需打包(bundle),缩短加载时间。
当为不同的平台进行开发时,最常见的问题,就是如何复用公共部分,并为平台相关的部分提供特别的实现。
根据平台的不同,动态地去切换代码的分支是一个最简单且符合直觉的做法。例如:
if (typeof window !== "undefined" && window.document) {
console.log('Hey browser!');
} else {
console.log('Hey Node.js!');
}
当然,这种做法也会有一些不便之处:
* 不同平台的代码放在同一个模块中,最终的 bundle 里无法到达(unreachable)的代码量大幅上升
* 过分使用会降低可读性
* 即使使用动态切换来加载不同的分支,仍会将所有的模块都打包到最终的 bundle 中(因为在构建时无法得知运行时的变量)
出于同样原因,使用变量来引入的模块有可能不被打包进 bundle 中:
// 构建时无法得知 moduleList 的内容
moduleList.forEach(function(module) {
require(module);
});
不过 Webpack 克服了其中的一些限制,例如它会猜测所有可能的值:
// Webpack 会将 controller 目录下的所有模块打包进 bundle
function getController(controllerName) {
return require("./controller/" + controllerName);
}
建议研读官方文档,查看更多支持的案例
先前提到,Webpack 还支持使用插件,来扩展我们的处理过程。下面使用两个内置插件DefinePlugin
UglifyJsPlugin
进行演示:
main.js
if (typeof __BROWSER__ !== "undefined") {
console.log('Hey browser!');
} else {
console.log('Hey Node.js!');
}
webpack.config.js
const path = require('path');
const webpack = require('webpack');
// 将 __BROWSER__ 替换成 “true”
const definePlugin = new webpack.DefinePlugin({
"__BROWSER__": "true"
});
const uglify = new webpack.optimize.UglifyJsPlugin({
// 保留空格和缩进
beautify: true,
// 去掉无法到达的代码
dead_code: true
});
module.exports = {
entry: path.join(__dirname, "src", "main.js"),
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js"
},
plugins: [definePlugin, uglify]
};
其中,DefinePlugin
搜索源文件,并将所有的 BROWSER 替换成“true”
即if (true !== “undefined”)
然后去除 dead code 之后,最后代码就变成了:
console.log('Hey browser!');
其中,DefinePlugin 还有很多不同的用法。例如还可以根据环境变量的内容,当前的时间戳,上一次 git commit 的 hash 值,当前用户,当前操作系统等。
大多数时候,在构建的时候我们就知道哪些代码是需要包括进 bundle 中的。
这时,我们可以在 webpack 中的配置文件中,批量切换模块:
const path = require('path');
const webpack = require('webpack');
// 将所有以 alertServer.js 结尾的文本替换成 ./alertBrowser.js
let moduleReplacementPlugin =
new webpack.NormalModuleReplacementPlugin(/alertServer.js$/, './alertBrowser.js');
module.exports = {
entry: path.join(__dirname, "src", "main.js"),
output: {
path: path.join(__dirname, "dist"),
filename: "bundle.js"
},
plugins: [moduleReplacementPlugin]
};
Webpack 的出色之处,在于它还会解析出完成的依赖树。例如如果切换的模块依赖 jQuery,它会将 jQuery 也自动打包到 bundle 中。
尽管各种设计模式都非常的有用,但更重要的是开发者如何选择最佳的方式去解决不同的问题。
以编写一个含服务端渲染的 react SPA 为例,记录一些要点
前端路由使用 HTML5 的 history API 时,后端需要提供相应的路由,这点可以通过复用路由实现。
先将路由的配置单独抽出来:
routesConfig.js
// components
const AuthorsIndex = require('./components/authorsIndex');
const AuthorPage = require('./components/authorPage');
const NotFound = require('./components/notFound');
// config
const routesConfig = [
{path: '/', component: AuthorsIndex},
{path: '/author/:id', component: AuthorPage},
{path: '*', component: NotFound}
];
然后后端路由引入同样的配置:
server.js
// ...
const ReactDom = require('react-dom/server');
// ...
// base on Express
app.get('*', (req, res) => {
// 用同样的路由配置匹配路径
Router.match({routes: routesConfig, location: req.url}, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
// 重定向
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
// 服务端渲染相应的页面
const markup = ReactDom.renderToString(<Router.RouterContext {...renderProps} />);
res.render('index', {markup});
} else {
res.status(404).send('Not found')
}
});
});
值得注意的是,在服务端中引入react-dom/server
作为 ReactDom
。浏览器收到这种方式渲染出来的页面时,就知道无需重复渲染组件,而只需要给现有的 DOM 节点附上事件监听器即可。
API 服务器用于根据特定的请求,返回相应的数据(一般是 JSON 格式)。
API 服务器应该完全和后端服务器分离,以便于独立扩容。最简单的方法是在另一个端口新建一个服务器(如后端用3000,API 服务器用3001)
但这样就会造成跨域问题,不过解决方案也很简单,只需要后端服务器接受所有请求,并且将 API 的请求代理到 API 服务器即可,如下图所示:
其中代理的部分可以使用http-proxy
模块轻松实现。
因为 API 要在两个不同的环境中使用,请求的路径也不同。
* 在浏览器端,使用 AJAX请求相对路径,如/api
* 服务端直接使用内置的 http
库或者封装好的request
模块请求 http://localhost:3001
在不同的环境请求相同的 API,需要编写不同的代码。为了减轻这方面的负担,axios
库将其封装成一致的接口,让请求 API 的代码也能在两端复用。
axios
还需要经过一点小小的封装才能直接复用:
const Axios = require('axios');
// 后端请求的是绝对路径,这里可以写成可传入参数配置的形式
const baseURL = typeof window !== 'undefined' ? '/api' : 'http://localhost:3001';
const xhrClient = Axios.create({baseURL});
module.exports = xhrClient;
非常简单但又非常有用的封装
React 中想使用异步组件的话,需要引入 react-router 的一个插件async-props
然后给原本的组件加上一个静态方法:
const React = require('react');
const Link = require('react-router').Link;
const xhrClient = require('../xhrClient');
class AuthorsIndex extends React.Component {
// context 指当时的 router,cb 用于通知 router 组件已经准备好了
static loadProps(context, cb) {
xhrClient.get('authors')
.then(response => {
const authors = response.data;
cb(null, {authors});
})
.catch(error => cb(error))
;
}
render() {
// ...
}
}
module.exports = AuthorsIndex;
客户端只需对 render 函数稍作修改:
const React = require('react');
// 引入 AsyncProps 组件
const AsyncProps = require('async-props').default;
const ReactRouter = require('react-router');
const Router = ReactRouter.Router;
const browserHistory = ReactRouter.browserHistory;
const routesConfig = require('./routesConfig');
class Routes extends React.Component {
render() {
return <Router
history={browserHistory}
routes={routesConfig}
// 修改 render 函数
render={(props) => <AsyncProps {...props}/>}
/>;
}
}
module.exports = Routes;
服务端渲染部分:
// ...
// 代理
const httpProxy = require('http-proxy');
// 异步组件支持
const AsyncProps = require('async-props').default;
const loadPropsOnServer = require('async-props').loadPropsOnServer;
// 创建 API 服务器的代理
const proxy = httpProxy.createProxyServer({
target: 'http://localhost:3001'
});
app.use('/api', (req, res) => {
proxy.web(req, res);
});
app.get('*', (req, res) => {
Router.match({routes: routesConfig, location: req.url}, (error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
// 因为需要等待异步数据到位,不能直接调用.renderToString()
loadPropsOnServer(renderProps, {}, (err, asyncProps, scriptTag) => {
const markup = ReactDom.renderToString(<AsyncProps {...renderProps} {...asyncProps} />);
// scriptTag 中的是保存起来的异步数据,让前端不用重复请求
res.render('index', {markup, scriptTag});
});
} else {
res.status(404).send('Not found')
}
});
});
// ...
值得注意的是 scriptTag, 里面包含的一些需要放在 HTML 中的 JavaScript 代码,主要是包含了一些服务端渲染时用到的异步数据,避免前端重复请求(类似于 Vue 中的 INITIAL_STATE)
scriptTag 在 ejs 模版中的放置:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>React Example - Authors archive</title>
</head>
<body>
<div id="main"><%- markup %></div>
<script src="/dist/bundle.js"></script>
<!-- scriptTag 的内容形如 window.asyncData = { key = value } -->
<%- scriptTag %>
</body>
</html>
至此基本包含了编写通用 JavaScript 应用需要注意的点。