SegmentFault 有赞美业前端团队最新的文章
2021-10-24T20:20:20+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
webpack 流程解析 (5) module reslove
https://segmentfault.com/a/1190000040856062
2021-10-24T20:20:20+08:00
2021-10-24T20:20:20+08:00
csywweb
https://segmentfault.com/u/csywweb
2
<h2>前言</h2><p>上文说道我们拿到了构建modlule的factory,和依赖等关键数据,通过addModuleTree经过<code>factorizeQueue</code>的控制走到了<code>factory.create</code>。这个时候就开始了reslove过程。<br>本文主要分析,<code>NormalModuleFactory</code> 内部 <code>beforeResolve</code>,<code>factorize</code>,<code>resolve</code>, <code>afterResolve</code> 这几个钩子。</p><h2>配置文件</h2><p>本文围绕的配置文件如下:</p><pre><code>module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.(js?|tsx?|ts?)$/,
use: [
{
loader: 'babel-loader',
},
],
},
]
},
resolve: {
extensions: ['.js', '.ts'],
alias: {
demo: path.resolve(__dirname, 'src/second'),
},
},
};</code></pre><h2>factory.create</h2><p>入口从<code>factory.create</code>开始,这里的 <code>factory</code> 是之前 <code>addModuleTree</code> 获取到的 <code>NormalModuleFactory</code><br><img src="/img/bVcVrWd" alt="image.png" title="image.png"></p><p><code>NormalModuleFactory</code> 先触发了其内部的 <code>beforeResolve</code> 钩子,然后在回调里执行了 <code>factorize</code> 钩子函数。<br><code>factorize</code> 钩子内有又调用了<code>resolve</code>。</p><p>这里看起来比较绕,简单解释一下:<br>钩子的调用顺序,就像是这样。<br><code>beforeResolve</code> -> <code>factorize</code> -> <code>resolve</code></p><ul><li>beforeResolve 没找到之前注册过的地方,看起来什么都没干,也有可能是我没找到</li><li>factorize 之前在 <code>ExternalModuleFactoryPlugin</code>插件中注册过,这里会处理下<code>external</code>的信息。</li><li><code>resolve</code> 钩子注册在 <code>NormalModuleFactory</code> 内部,用于解析这个module,生成对应的loader和依赖信息,这里的重点就在<code>resolve</code></li></ul><h2>resolve</h2><h3>getLoaderResolver</h3><p>进来的第一步<code>resolve</code> 钩子先调用了<code>this.getResolver("loader")</code> 返回loaderResolver,这个可以理解为是解析loader的方法。<br>简单过一下分为以下几步:</p><ul><li>调用到了 <code>ResolverFactory</code>里的<code>get</code>方法</li><li>判断是否有对应类型的缓存</li><li>创建 <code>resolveOptions</code>,</li><li>调用 <code>require("enhanced-resolve").ResolverFactory</code> 创建了一个 <code>resolver</code>,然后返回 <code>NormalModuleFactory</code>继续执行代码。</li></ul><pre><code>const loaderResolver = this.getResolver("loader");</code></pre><p><code>loaderResolver</code>暴露了一个<code>resolver</code>方法,用于解析<code>loader</code>。</p><h3>normalResolver</h3><p>接着往下走,略过一些判断,直接走到了<code>defaultResolve</code>这个方法,这里会根据webpack配置文件中的<code>resolve</code>选项,生成一个 <code>normalResolver</code>。同样的,这个<code>normalResolver</code>也是<code>require("enhanced-resolve").ResolverFactory</code>的实例,也暴露出了一个<code>resolve</code>方法。</p><pre><code class="javascript">const normalResolver = this.getResolver(
"normal",
dependencyType
? cachedSetProperty(
resolveOptions || EMPTY_RESOLVE_OPTIONS,
"dependencyType",
dependencyType
)
: resolveOptions
);</code></pre><p>接下来会把这个<code>normalResolver</code> 和一些上下文信息传给<code>resolveResource</code>方法,这里最终会调用到<code>node_modules/enhanced-resolve/lib/Resolver.js</code>的<code>doResolve</code>。</p><pre><code>this.resolveResource(
contextInfo,
context,
unresolvedResource,
normalResolver,
resolveContext,
(err, resolvedResource, resolvedResourceResolveData) => {
if (err) return continueCallback(err);
if (resolvedResource !== false) {
resourceData = {
resource: resolvedResource,
data: resolvedResourceResolveData,
...cacheParseResource(resolvedResource)
};
}
continueCallback();
}
);</code></pre><p>然后根据<code>doResolve</code>返回的<code>resolvedResource</code>和<code>resolvedResourceResolveData</code>一起拼装成<code>resourceData</code>。我们在后续解析loader的时候还会用到这个。</p><p><code>resourceData</code>数据结构<br><img src="/img/bVcVADb" alt="image.png" title="image.png"></p><h3>解析loader</h3><p>在<code>resolvedResource</code>的回调里继续执行</p><pre><code>const result = this.ruleSet.exec({
resource: resourceDataForRules.path,
realResource: resourceData.path,
resourceQuery: resourceDataForRules.query,
resourceFragment: resourceDataForRules.fragment,
scheme,
assertions,
mimetype: matchResourceData
? ""
: resourceData.data.mimetype || "",
dependency: dependencyType,
descriptionData: matchResourceData
? undefined
: resourceData.data.descriptionFileData,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler,
issuerLayer: contextInfo.issuerLayer || ""
});</code></pre><p>这里会根据配置文件里的<code>rules</code>得到需要的loader,这个例子里,我们的<code>result</code>是<br><img src="/img/bVcVAEM" alt="image.png" title="image.png"></p><p>接下来会通过这个<code>result</code>的遍历,生成<code>useLoadersPost</code>, <code>useLoaders</code>, <code>useLoadersPre</code>。<br>然后调用<code>resolveRequestArray</code>得到<code>postLoaders, normalLoaders, preLoaders</code>。</p><pre><code>this.resolveRequestArray(
contextInfo,
this.context,
useLoaders,
loaderResolver,
resolveContext,
(err, result) => {
normalLoaders = result;
continueCallback(err);
}
);</code></pre><p>当前例子并没有<code>postLoaders</code>和<code>preLoaders</code>,这里只有<code>normalLoaders</code>。<code>resolveRequestArray</code>内部调用<code>loaderResolver.resolve</code>解析<code>useLoaders</code>,最后结果就是把<code>result</code>里的loader替换成了对应的真实文件地址。</p><pre><code>{
ident:undefined
loader:'/Users/csy/Code/webpack5/node_modules/babel-loader/lib/index.js'
options:undefined
}</code></pre><h3>生成回调数据</h3><p>最后在<code>continueCallback</code>处理下已经生成好的数据,首先是对<code>loader</code>的合并。把<code>postLoaders, normalLoaders, preLoaders</code>这几个合并。然后<code>assign</code>一下<code>data.createData</code>, 这个<code>data</code>来自于钩子的入口传入的data。</p><pre><code>Object.assign(data.createData, {
layer:
layer === undefined ? contextInfo.issuerLayer || null : layer,
request: stringifyLoadersAndResource(
allLoaders,
resourceData.resource
),
userRequest,
rawRequest: request,
loaders: allLoaders,
resource: resourceData.resource,
context:
resourceData.context || getContext(resourceData.resource),
matchResource: matchResourceData
? matchResourceData.resource
: undefined,
resourceResolveData: resourceData.data,
settings,
type,
parser: this.getParser(type, settings.parser),
parserOptions: settings.parser,
generator: this.getGenerator(type, settings.generator),
generatorOptions: settings.generator,
resolveOptions
});</code></pre><p>这里着重讲一下<code>getParser</code>和<code>getGenerator</code>, 这两个方法返回的是对应文件的解析器和构建模板的方法。按照当前示例,返回的是<code>JavascriptParser</code>和<code>JavascriptGenerator</code>。</p><p>然后这个<code>createData</code>将被用于<code>createModule</code>。<br>在执行完<code>NormalModuleFactory</code>的<code>afterResolve</code>钩子后</p><pre><code>const createData = resolveData.createData;
this.hooks.createModule.callAsync(//something)</code></pre><p>reslove结束了,即将开始下一步,创建<code>module</code>!</p><h2>小结</h2><ul><li>module resolve 流程用于获得各 loader 和模块的绝对路径等信息。</li><li>在 <code>resolver</code>钩子里,先通过 enhanced-resolve 获取 <code>loaderResolver</code>,提供 resolve 方法</li><li>在 <code>defaultResolve</code> 方法里,获取 <code>normalResolver</code>, 提供 resolve 方法。</li><li>解析 <code>unresolvedResource</code>,得到文件的绝对路径等信息</li><li>根据<code>rules</code>得到 loader</li><li>使用 <code>loaderResolver</code> 得到loader的绝对路径等信息</li><li>合并 loader, 拼接数据,</li><li>调用 <code>NormalModuleFactory</code>的<code>afterResolve</code>钩子,结束 <code>resolve</code> 流程。</li></ul>
webpack 流程解析(4): 开始构建
https://segmentfault.com/a/1190000040821096
2021-10-16T17:26:11+08:00
2021-10-16T17:26:11+08:00
csywweb
https://segmentfault.com/u/csywweb
0
<h2>前言</h2><p>准备工作做了三遍文章,现在、立刻、马上,我们进入构建流程的分析!</p><h2>构建入口</h2><p>这个过程还是在<code>compiler.compiler</code>函数里,</p><pre><code> // 在这之前new了一个 compilation 对象
this.hooks.make.callAsync(compilation, err => {
logger.timeEnd("make hook");
if (err) return callback(err);
logger.time("finish make hook");
this.hooks.finishMake.callAsync(compilation, err => {
logger.timeEnd("finish make hook");
if (err) return callback(err);
process.nextTick(() => {
logger.time("finish compilation");
compilation.finish(err => {
logger.timeEnd("finish compilation"); if (err) return callback(err);
logger.time("seal compilation");
compilation.seal(err => {
//dosomething
});
});
});
});
});</code></pre><p>这里触发了<code>make</code>钩子注册的回调,还记得我在<a href="https://segmentfault.com/a/1190000040781672">初始化</a>部分提到的<code>EntryPlugin</code>吗?在这里注册了一个钩子回调,触发了 compilation.addEntry</p><pre><code>compilation.addEntry(context, dependency, name, callback); //其中 dependency 为 EntryDependency 实例。</code></pre><h3>addEntry</h3><p>addEntry 做了这么几件事:</p><ul><li>生成 EntryData</li><li>调用<code>compilation</code>钩子<code>addEntry</code></li><li>执行 compilation.addModule</li></ul><h3>addModule</h3><p>addModule 根据dep,拿到对应的 moduleFactory, 然后执行<code>handleModuleCreation</code>, 把 <code>moduleFactory</code>和<code>dependency</code>等数据塞入一个队列<code>factorizeQueue</code></p><p><strong>获取moduleFactory</strong></p><pre><code>const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);</code></pre><p><code>this.dependencyFactories</code>是一个 Map, 那么他是什么时候set的呢?答案还是在<a href="https://segmentfault.com/a/1190000040781672">初始化</a>部分提到的<code>EntryPlugin</code>中。 </p><p>** 塞入队列</p><p>获取到依赖和模块的编译方法之后,塞入<code>factorizeQueue</code>队列</p><pre><code>this.factorizeModule({
currentProfile,
factory,
dependencies,
factoryResult: true,
originModule,
contextInfo,
context
},
() => { // dosomethine})</code></pre><pre><code class="javascript">// Workaround for typescript as it doesn't support function overloading in jsdoc within a class
Compilation.prototype.factorizeModule =
/** @type {{
(options: FactorizeModuleOptions & { factoryResult?: false }, callback: ModuleCallback): void;
(options: FactorizeModuleOptions & { factoryResult: true }, callback: ModuleFactoryResultCallback): void;
}} */
(
function (options, callback) {
this.factorizeQueue.add(options, callback);
}
);</code></pre><p>看到这里,有点没有头绪,add之后在整个 <code>compilation</code> 里没有找到类似于 <em>factorizeQueue.start</em>,<em>factorizeQueue.run</em> 之类的代码。一起去看看<code>factorizeQueue</code> 内部干了啥</p><h3>factorizeQueue</h3><pre><code>this.factorizeQueue = new AsyncQueue({
name: "factorize",
parent: this.addModuleQueue,
processor: this._factorizeModule.bind(this)
});</code></pre><p><code>factorizeQueue</code> 是 <code>AsyncQueue</code> 的实例。<code>AsyncQueue</code>主要是做了一个队列控制。队列长度根据外部传入的<code>parallelism</code>来控制,<code>factorizeQueue</code>没有传,这里默认为1。</p><p>如果条件ok,在<code>AsyncQueue</code>的内部会调用<code>_processor</code></p><pre><code>this._processor(entry.item, (e, r) => {
inCallback = true;
this._handleResult(entry, e, r);
});</code></pre><p>这里就调用到<code>_factorizeModule</code>,接下来执行<code>factory.create</code>,开始<strong>reslove!</strong></p><h2>结语</h2><p>到这里我们已经了解到<code>webpack</code>是如何使用配置中的<code>entry</code>属性,获取到modulefactory,下一篇将介绍<strong>reslove</strong>过程。</p>
webpack 流程解析(3) 创建compilation对象
https://segmentfault.com/a/1190000040793136
2021-10-11T13:21:22+08:00
2021-10-11T13:21:22+08:00
csywweb
https://segmentfault.com/u/csywweb
0
<h2>前言</h2><p>webpack初始化完成之后,则会通过传入的<code>options.watch</code>来判断是否要开启watch,如果开启<code>watch</code>则会执行<code>watch</code>的流程,如果是<code>run</code>,则会执行<code>run</code>的流程,本系列只关注主线,所以我们直接从<code>run</code>开始,<code>watch</code>感兴趣的同学可以自行研究研究</p><h2>compiler.run()</h2><p>直接看核心代码</p><pre><code>const run = () => {
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
};</code></pre><p>简单说就是做了这么几件事。</p><ul><li>触发<code>beforeRun</code>的回调</li><li>触发<code>run</code>的回调</li><li>然后调<code>this.readRecords</code></li><li>在<code>readRecords</code>的回调里调用<code>this.compile(onCompiled)</code>开启编译</li></ul><p>我们一步一步看<br><code>beforeRun</code>会触发之前在<code>NodeEnvironmentPlugin</code>中注册的 beforeRun 钩子,这个plugin会判断<code>inputFileSystem</code>是否被配置,如果没有配置则执行<code>purge</code>清理方法。</p><p><code>readRecords</code>会读取一些统计信息,由于没有配置<code>recordsInputPath</code>,这里会把<code>this.records</code>初始为<code>{}</code>。</p><h2>创建compilation实例</h2><p>接下来就执行到<code>compiler.compiler()</code>方法。<br><code>compiler.compiler</code>方法贯穿了整个编译过程。首先<code>compiler</code>实例化了一个<code>compilation</code>。</p><pre><code>compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
// do something
}
}</code></pre><h3>获取参数</h3><pre><code>newCompilationParams() {
const params = {
normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory()
};
return params;
}</code></pre><p>参数有两个,一个是<code>NormalModuleFactory</code>的实例,一个是<code>ContextModuleFactory</code>的实例。<code>ContextModuleFactory</code>这个参数我在compilation里面没搜到,暂且略过,后续打个断点看会不会走进来。这里主要看下<code>NormalModuleFactory</code>。</p><h4>NormalModuleFactory</h4><p>先看下实例化<code>NormalModuleFactory</code>的参数</p><pre><code>const normalModuleFactory = new NormalModuleFactory({
context: this.options.context,
fs: this.inputFileSystem,
resolverFactory: this.resolverFactory,
options: this.options.module,
associatedObjectForCache: this.root,
layers: this.options.experiments.layers
});</code></pre><p>注意这里的<code>resolverFactory</code>, 以后会用到。<br>接下来看下<code>new NormalModuleFactory</code>的时候发生了啥</p><pre><code>constructor({
context,
fs,
resolverFactory,
options,
associatedObjectForCache,
layers = false
}) {
super();
this.hooks = 定义了很多hooks
this.resolverFactory = resolverFactory;
this.ruleSet = ruleSetCompiler.compile([
{
rules: options.defaultRules
},
{
rules: options.rules
}
]);
this.context = context || "";
this.fs = fs;
this._globalParserOptions = options.parser;
this._globalGeneratorOptions = options.generator;
/** @type {Map<string, WeakMap<Object, TODO>>} */
this.parserCache = new Map();
/** @type {Map<string, WeakMap<Object, Generator>>} */
this.generatorCache = new Map();
/** @type {Set<Module>} */
this._restoredUnsafeCacheEntries = new Set();
const cacheParseResource = parseResource.bindCache(
associatedObjectForCache
);
this.hooks.factorize.tapAsync(
// do something
);
this.hooks.resolve.tapAsync(
// dosomething
);
}</code></pre><p>可能觉得太长了不看,我直接给大家翻译一下干了啥</p><ul><li>定义了很多内部的hook,比方说最后注册的两个 reslover,factorize</li><li>定义了很多构建module需要的变量,这里先不细说。</li><li>同时注册了两个<code>NormalModuleFactory</code>的内部 hook。会在合适的时机在被compilation对象调用</li></ul><p><code>new NormalModuleFactory()</code>之后,触发了compiler上的<code>normalModuleFactory</code>钩子</p><pre><code>this.hooks.normalModuleFactory.call(normalModuleFactory);</code></pre><h3>继续触发钩子回调</h3><p>然后触发<code>beforeCompile</code>和<code>compile</code>钩子。</p><h3>开始实例化</h3><pre><code>newCompilation(params) {
const compilation = this.createCompilation(params); //这里简单理解为new 了一下,
compilation.name = this.name;
compilation.records = this.records;
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}</code></pre><p>分类一下,这个函数做了两件事。</p><ul><li>new Compilation,再赋一点值</li><li><p>注册两个钩子</p><h4>new Compilation 内部细节</h4><p>Compilation对象表示了当前模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,代表了一次资源的构建。constructor代码太多就不贴在这里了,大家可以自行去看看。</p></li></ul><p>简单总结一下,就是</p><ul><li>在compilation内部注册了很多内部的钩子。</li><li>初始化了一些自身属性</li><li>实例化<code>MainTemplate</code>,<code>ChunkTemplate</code>,<code>HotUpdateChunkTemplate</code>, <code>RuntimeTemplate</code>, <code>ModuleTemplate</code>。用于提供编译模板</li></ul><h3>实例化后的钩子调用</h3><pre><code>this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);</code></pre><p><code>compilation</code>钩子的调用,会调用到之前在entryplugin注册的方法。<br>会往dependencyFactories里增加依赖模块。</p><pre><code>compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);</code></pre><blockquote>也许你好奇,这里为什么有两个钩子?原因是跟子编译器有关。在 Compiler 的 createChildCompiler 方法里创建子编译器,其中 thisCompilation 钩子不会被复制,而 compilation 会被复制。<br>子编译器拥有完整的 module 和 chunk 生成,通过子编译器可以独立于父编译器执行一个核心构建流程,额外生成一些需要的 module 和 chunk。</blockquote><h2>结束</h2><p>到目前为止,描述构建流程的对象<code>compiler</code>和描述编译过程的对象<code>compilation</code> 对象已经创建完成。下一篇文章我们进入构建流程。</p>
webpack 流程解析(2):参数初始化完成
https://segmentfault.com/a/1190000040781672
2021-10-08T20:57:15+08:00
2021-10-08T20:57:15+08:00
csywweb
https://segmentfault.com/u/csywweb
1
<h2>前言</h2><p>上文说到 <code>webpack</code> 准备好了参数,要创建 <code>compiler</code>对象了。<br>创建完之后,则会执行 <code>compiler.run</code> 来开始编译,本文将阐述 <code>new Compiler</code> 到 <code>compiler.run()</code>中间的过程。<br>整体过程都发生在<code>createCompiler</code>这个函数体内。</p><pre><code>/**
* @param {WebpackOptions} rawOptions options object
* @returns {Compiler} a compiler
*/
const createCompiler = rawOptions => {</code></pre><h2>new Compiler</h2><ul><li><p>在new之前,webpack会完成一次基础参数的初始化,这里只给日志输出格式和context进行了赋值</p><pre><code>applyWebpackOptionsBaseDefaults(options);</code></pre></li><li><code>webpack/lib/Compiler.js</code> 是整个webpack的编译核心流程。</li><li><code>new Compiler</code> 的时候先在<code>Tapable</code>注册了一堆钩子,例如常见的<em>watch-run</em>,<em>run</em>, <em>before-run</em>, 等等。更多的钩子可以在<a href="https://link.segmentfault.com/?enc=fwW3zmmp9yq%2BJXsCU2DJzQ%3D%3D.Ts68gY3%2FrK622pe4IODl%2FN7XOiAq8vpP2WYD5hF5fsSjsDKz1P3QIVlrrRrQdOuPwlzzlfKds6wnlyBTRg2LcQ%3D%3D" rel="nofollow">这里查看</a>。</li></ul><h2>初始化文件操作</h2><pre><code>new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);</code></pre><p>这里是在拓展compiler对象,增加对文件的一些操作,例如<code>输入</code>,<code>输出</code>,<code>监听</code>,<code>缓存</code>等方法。同时还注册了一个<code>beforeRun</code>钩子的回调。</p><pre><code>apply(compiler) {
const { infrastructureLogging } = this.options;
compiler.infrastructureLogger = createConsoleLogger({
level: infrastructureLogging.level || "info",
debug: infrastructureLogging.debug || false,
console:
infrastructureLogging.console ||
nodeConsole({
colors: infrastructureLogging.colors,
appendOnly: infrastructureLogging.appendOnly,
stream: infrastructureLogging.stream
})
});
compiler.inputFileSystem = new CachedInputFileSystem(fs, 60000);
const inputFileSystem = compiler.inputFileSystem;
compiler.outputFileSystem = fs;
compiler.intermediateFileSystem = fs;
compiler.watchFileSystem = new NodeWatchFileSystem(
compiler.inputFileSystem
);
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) {
compiler.fsStartTime = Date.now();
inputFileSystem.purge();
}
});
}</code></pre><blockquote>这里的fs,不是nodejs的 file system,是用了一个第三方包<code>graceful-fs</code></blockquote><h2>注册插件</h2><pre><code>if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}</code></pre><p>接下来webpack会把options注册的插件,都注册一遍。传入<code>compiler</code>对象给插件内部使用,插件通过 <code>compiler</code> 提供的hook,可以在编译全流程注册钩子的回调函数。同时有些<code>compiler</code>钩子又传入了<code>compilation</code>对象,又可以在资源构建的时候注册 <code>compilation</code> 钩子回调。</p><p><a href="https://link.segmentfault.com/?enc=367v7G6sag0w7E9OWTWbIA%3D%3D.qo4xbc65sGUfRQDYvrBkj8rQcUtKBt0w4HpAey34mwZjqNWKz7XH0ksz0kqCukRiUO9Wb73Y%2FG1%2FhPLxUaGuvw%3D%3D" rel="nofollow">如何编写一个webpack插件</a></p><h2>environment ready</h2><p>插件注册完之后,webpack 又一次给options赋值了一次默认参数。为什么和前面的<code>applyWebpackOptionsBaseDefaults</code>一起呢。<br>这里调用了</p><pre><code>applyWebpackOptionsDefaults(options);</code></pre><p>又加了一波默认值。<br>加完之后调用了<code>environment</code>和<code>afterEnvironment</code>两个钩子。</p><pre><code>compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();</code></pre><h2>注册内置插件</h2><p>环境初始化之后,webpack还需要执行一下它自己内部的默认插件。</p><pre><code>new WebpackOptionsApply().process(options, compiler);</code></pre><p>这里会根据你的配置,执行对应的插件。<br>挑几个和钩子有关系的讲讲,</p><h3>解析entry</h3><pre><code>new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry); </code></pre><p>这里就是生成构建所需的entry数据结构</p><pre><code>/** @type {EntryOptions} */
const options = {
name,
filename: desc.filename,
runtime: desc.runtime,
layer: desc.layer,
dependOn: desc.dependOn,
publicPath: desc.publicPath,
chunkLoading: desc.chunkLoading,
wasmLoading: desc.wasmLoading,
library: desc.library
};</code></pre><p>然后再调用<code>EntryPlugin</code>在<code>applay</code>方法里注册Compiler.hooks:compilation, make 这两个钩子函数。将来等<code>compiler</code>对象里面,触发<code>make</code>钩子的时候,在<code>EntryPlugin</code>注册的回调会触发<code>complition.addEntry(context, dep, options)</code>开始编译<br><strong>这里是重点,不然找不到开始的入口</strong></p><pre><code>compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
}
);</code></pre><h3>注册resloverFactory钩子</h3><p>webpack本身的一些插件调用完成之后,会调用<code>afterPlugin</code>这个钩子。</p><pre><code>compiler.hooks.afterPlugins.call(compiler);</code></pre><p>接下来 webpack 在 compiler.resolverFactory 上注册了<code>resolveOptions</code>钩子</p><pre><code>compiler.resolverFactory.hooks.resolveOptions
.for("normal")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
compiler.resolverFactory.hooks.resolveOptions
.for("context")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
resolveOptions.resolveToContext = true;
return resolveOptions;
});
compiler.resolverFactory.hooks.resolveOptions
.for("loader")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});</code></pre><p>这里的目的是为 <code>Factory.createResolver</code> 提供默认的参数对象(含有相关的 resolve 项目配置项)。<br>然后再调用<code>afterResolvers</code>钩子</p><pre><code>compiler.hooks.afterResolvers.call(compiler);</code></pre><h3>初始化完成</h3><p>到目前为止,compiler 对象上已经有了足够的东西开始我们的编译,就告诉外界初始化完成,以webpack的调性,必然还有一个钩子的触发。</p><pre><code>compiler.hooks.initialize.call();</code></pre><h2>结语</h2><p>webpack 编译前的所有事情已经交待清楚,下一篇将开始启动编译。<br>文章中提到的各种钩子的注册,烦请读者记下来,后续在整个编译过程中,前面注册的一些钩子,经常会在你遗漏的地方触发,这也是调试webpack过程中的一个痛点。</p>
webpack 流程解析(1):小弟先帮我看看对不对之 weback-cli
https://segmentfault.com/a/1190000040774804
2021-10-07T00:38:18+08:00
2021-10-07T00:38:18+08:00
csywweb
https://segmentfault.com/u/csywweb
0
<h2>前言</h2><p>compiler对象是一个全局单例,它负责把控整个webpack打包的构建流程。<br>本文将会介绍在 new compiler 之前,webpack做了什么</p><h2>启动webpack</h2><p>通常情况下,我们使用如下方式来启动webpack</p><pre><code>// package.json
script: {
"start": "webpack"
}</code></pre><h2>webpack/bin</h2><p>运行 <code>npm run start</code> 之后,会先进入 <code>webpack/bin</code> 下, webpack使用</p><pre><code>isInstalled("webpack-cli")</code></pre><p>来判断是否安装了<code>cli</code>, 没安装会使用 <code>yarn</code> 或者 <code>npm</code> 帮你安装,最后会走到<code>runCli</code>这个方法,核心代码就一句</p><pre><code>require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));</code></pre><p>这里去读取 <code>webpack-cli/bin/cli.js</code>。</p><h2>webpack-cli/bin</h2><p><code>webpack-cli/bin/cli.js</code> 居然还要判断一下是否安装了 <code>webpack</code>(上面不是判断过了), 如果没安装再帮你安装一下,然后就是实例化一个 <code>webpack-cli</code> 对象,执行实例的<code>run</code>方法。<br>这里面代码就不细说了,其实就干了两件事:</p><ul><li>拿到process.args 的参数,校验</li><li>合并参数,针对args的值给webpack的config增加对应的plugin</li></ul><p>最后拿到了参数又调用了<code>webpack</code>。</p><blockquote>这里用了两个包来提高运行效率,一个是 <code>import-local</code>,用于优先使用本地文件,另一个是 <code>v8-compile-cache</code>, 用来做 v8 的编译缓存优化。后续我们再聊这两个</blockquote><h2>回到webpack</h2><p>回到webpack之后呢,就要开始创建 <code>compiler</code> 实例啦,在这之前,其实也会有一点分支逻辑需要处理</p><ul><li>如果参数是一个数组,就创建MultiCompiler实例, 否则就创建一个Compiler的实例。</li><li>参数的校验和复制默认值</li></ul><p>一切都走完之后,就要开始创建 compiler 对象了。</p><h2>结语</h2><p>在创建 <code>compiler</code> 对象之前 webpack 做的事情并不多。简单说就一句话,<strong>准备好参数</strong>。同时也有一些性能优化的手段,这些不在本次系列的讨论范围内,以后有时间再和大家分享</p>
大拇指创新实验室月刊(第一期)
https://segmentfault.com/a/1190000040115630
2021-06-03T17:35:13+08:00
2021-06-03T17:35:13+08:00
大宝剑
https://segmentfault.com/u/qianduandabaojian
9
<p><img src="/img/bVcSdBf" alt="" title=""></p><h2>新闻速报</h2><ul><li><a href="https://link.segmentfault.com/?enc=N2i12bRgKadQ5Jk3kMadHw%3D%3D.EmzaVB6g5Vl%2FXuhDm5lsU3VkXZnKA%2FOWFYdJHObZJv2kYeHw1QiXznDfll8pwUNP" rel="nofollow">Babel发布7.14.0</a></li><li><a href="https://link.segmentfault.com/?enc=mMhFEACjNpMalRnTBRDJzw%3D%3D.AgUc2XCCXpGiBbu20BqXL3xnt9aNaMLs6Ic2BAblAcRB%2FxA0agW9%2FAFPT6Wlm3lxNqoOWyJPGrqSbzzL7KJN5eWUsKoJ7c2sRwNbNzvgjpnrg%2BY%2B6njq9vo4s%2B1HXVlL" rel="nofollow">Node.js发布16.0.0</a></li><li><a href="https://link.segmentfault.com/?enc=DiXOMkvYYPjlfzbdoKUr8w%3D%3D.Ru90AC8f1p2chXEM0fCvmIejeeEKyZbMtlkA5EWcLBpj%2BqKAXUnwMgKo%2Brv30E4FfWQScT545DlZHAmEa6c72Q%3D%3D" rel="nofollow">Bootstrap 5发布</a></li><li><a href="https://link.segmentfault.com/?enc=sospQ4JriWtTbnwBpcFJmg%3D%3D.Y5PapjV98DgcKn%2F7K6BwjTnQOhRCvjv85wY%2Brceduw1h97oOif5lr5ArlofYCJqaJqvhVQTVx44yGjcmv6WpoQ%3D%3D" rel="nofollow">ES2021/ES12新特性</a></li><li><a href="https://link.segmentfault.com/?enc=9%2BDKLHijVK68M%2BhnrECwDA%3D%3D.YnUcgY%2Frc0Op0mmKq9KIuJ0HojCfzTe3mGPvIjSFenHR%2B%2Fa5MAd24IFEjVGLfw86rzZ%2B4ksuc09PhIMkUOXiDw%3D%3D" rel="nofollow">Chrome 89 devtools增加新特性</a></li></ul><h2>前端生态</h2><h4>微信新能力</h4><p><strong>1. 小程序直播新增【智能助理】功能</strong></p><blockquote>智能助理是由小程序直播与微信对话开放平台联合推出商家运营工具,支持直播间「欢迎语,智能问答,热问追踪、销售线索统计分析」等能力,可有效解决主播面对海量用户的互动难题,提升带货效率。</blockquote><pre><code> 小程序直播组件已更新至 1.2.9 版本,新增智能助理功能,请及时更新组件版本,确保能使用新功能。智能助理功能目前处于内测期间,
智能助理内测指引:https://developers.weixin.qq.com/community/develop/article/doc/0002c0811dc7d09effcb4a4e556413 </code></pre><p>功能使用说明: <a href="https://link.segmentfault.com/?enc=lQNlxBiHSHqtXuJhuhT%2BvA%3D%3D.xfUtXMPyerXwI0915%2Fcd9gcNzEVqoNyL74Sn4Js4eUre8vX%2FSWscxPxlq5%2F2pVNR" rel="nofollow">https://docs.qq.com/doc/DTkZQ...</a></p><p><strong>2. 微信长链转短链接口停止生成短链</strong></p><blockquote>平台将对2021年3月15日之后停止该接口新生成的短链的能力,已生成的短链暂不受影响(预计下半年停止历史生成短链接解析服务)</blockquote><pre><code> 长链接转短链接服务致力于优化用户体验,在微信中提升扫码速度和成功率,解决开发者原链接(商品、支付二维码等)太长导致微信扫码速度和成功率下降的问题。但随着技术的发展,微信扫码能力已有较大提升,不再需要对原始链接进行转换。
</code></pre><p>接口说明: <a href="https://link.segmentfault.com/?enc=xN7E87I9NdZeImyo8oX8Mg%3D%3D.rT8vTBY4HFROKpyrfpSq6BrRcDSC%2BB6V0BzcShlk64aqBsGo7%2BYilJdH9lHBsUtoMKrdxaE5AtjLcHhd5sCxsUa%2BCxuO1l7%2B0FW4dH4XnB3WHY%2FVW8jV05rqr2Shp7Be" rel="nofollow">https://developers.weixin.qq....</a></p><p><strong>3. 微信卡券将不再支持新创建“优惠券”</strong></p><blockquote>2021年4月1日0点起,“微信卡券>优惠券”将不再支持新创建优惠券,该功能后续将陆续下线</blockquote><pre><code> 因“微信卡券>优惠券”产品能力未来将统一升级为“微信支付优惠券”,相关功能将进行逐步调整。2021年4月1日0点起,“微信卡券>优惠券”将不再支持新创建优惠券,该功能后续将陆续下线。其他微信卡券功能暂无变化。本次调整详细内容如下:
1.4月1日0点起,已开通微信卡券功能的商户将无法新创建优惠券,包含API接口创建与公众平台页面创建;商户使用“会员卡”、“礼品卡”或“票证”等能力不受影响;
2.历史已创建的优惠券,可继续正常使用“微信卡券>优惠券”相关功能(包含发放、核销等操作);</code></pre><p>了解更多: <a href="https://link.segmentfault.com/?enc=O1IjcSVQDH9XAxCWb7BmKQ%3D%3D.yplVELhEvDKkhbg1T1N8bOqRLs21I6Z6DepbBUv9%2FAWzmywDq%2BbeZas8oHktZI18GzgfDaZ%2B%2BO2nEkCm2lfTrA%3D%3D" rel="nofollow">https://docs.qq.com/doc/DVm13...</a></p><p><strong>4. 微信视频号和微信公众号关联</strong></p><blockquote>视频号主页和公众号主页可以关联显示的功能了</blockquote><pre><code> 1、点击个人视频号页面右上角的「…」进入视频号设置页面,点击最下方的「账号管理」即可绑定公众号。值得一提的是,在企业/机构视频号中,只能绑定相同主体的公众号;在个人视频号中,公众号需是相同的管理员才可绑定。
2、视频号、公众号完成绑定后,公众号的粉丝就能在公众号主页看到关注账号的视频号信息,点击即可直达视频号页面关注,同时视频号粉丝也能在视频号主页看到该账号的公众号信息,关注更方便了。
</code></pre><h4>web-vitals</h4><blockquote>优化用户体验的质量一直都是是每个 Web 站点长期成功的关键,Web Vitals是谷歌2020年新出台的一套网页核心的性能指标体系。</blockquote><p><img src="/img/bVcSd6G" alt="" title=""></p><h4>Node.js 16新能力</h4><p><strong>1. Timers Promise API</strong></p><blockquote>Timers Promise API 其实在 Node15 就已存在,那时候是一个实验特性,目前已进入了稳定阶段,是一项令人兴奋的特性。那它到底是干什么用的呢?</blockquote><pre><code class="javascript">import { setTimeout } from 'timers/promises'
await setTimeout(100)
再比如
import { setInterval } from 'timers/promises'
for await (const startTime of setInterval(100, Date.now())) {
const now = Date.now()
if ((now - startTime) > 1000)
break
}</code></pre><p><strong>2. 底层依赖升级</strong></p><blockquote>v8, 升级到 9.0,主要是 ECMAScript RegExp Match Indices<br>llhttp, 升级到 6.0.0,用以解析 HTTP 报文<br>icu, 升级到 69.1<br>npm, 升级到 7.10.0</blockquote><pre><code class="javascript">使用 process.versions 可看到相关依赖的版本号
> process.versions
{
node: '16.0.0',
v8: '9.0.257.17-node.10',
uv: '1.41.0',
zlib: '1.2.11',
brotli: '1.0.9',
ares: '1.17.1',
modules: '93',
nghttp2: '1.42.0',
napi: '8',
llhttp: '6.0.0',
openssl: '1.1.1k+quic',
cldr: '39.0',
icu: '69.1',
tz: '2021a',
unicode: '13.0',
ngtcp2: '0.1.0-DEV',
nghttp3: '0.1.0-DEV'
}</code></pre><p><strong>3. btoa 与 atob</strong></p><blockquote>关于 Base64 的转化,Node 在以前使用了 Buffer.from,而现在支持 btoa/atob 与浏览器环境保持了一致。<br>而对于一个 SSR 项目而言,执行环境的区分将无关紧要,统一使用 btoa/atob 就好了</blockquote><pre><code class="javascript">const base64 = {
encode (v: string) {
return isBrowser ? btoa(v) : Buffer.from(v).toString('base64')
},
decode (v: string) {
return isBrowser ? atob(v) : Buffer.from(v, 'base64').toString()
}
}</code></pre><p><strong>4. 原生支持 Mac 电脑的 M1 芯片</strong></p><ul><li>【Node.js学习路线】 - <a href="https://link.segmentfault.com/?enc=4o9ZTTAoVnSLOCLn69bKww%3D%3D.nGI1zUuGWnK2bjeKE1pRI5e2DSChLQoxldkpU9KfD8Evh2qgrrTc5DU0kfLSR3OraKpIZtqLAUiErosxldEyvj4LDOnjL3juij%2FlQVkhKDiBmj1eE5XWrclH9WvT2wM%2B%2ByYX58CEHAVU3YDCR0ZB0s524eO1gcpj4uJ6EHvOE9YMFAonTSfrY2zcbJflPiQ32o1d6%2F4tm2ms7offRRouDg%3D%3D" rel="nofollow">https://static.app.yinxiang.c...</a></li></ul><h4>Node.js学习路线</h4><pre><code>必备技能
* Javascript
* npm软件包管理
* Node.js基础知识
* 时间发射器(Event Emitter)
* 回调
* Buffer类
* 模块系统(Module System)
开发技能
* 版本管理系统
* HTTP/HTTPS协议
Web框架
* Express.js
* Meteor.js
* Sails.js
* Koa.js
* Nest.js
数据库管理
* 关系数据库管理系统
* SQL Server
* MySQL
* PostgreSQL
* MariaDB
* 云数据库服务
* Azure CosmosDB
* Amazon DynamoDB
* NoSQL 数据库
* MongoDB
* Redis
* Apache Cassandra
* LiteDB
* 搜索引擎
* ElasticSearch
* Solr
缓存
* 内存缓存(节点缓存node-cache/内存缓存memory-cache)
* 分布式缓存(Redis/Memcached)
模板引擎
* Mustache.js
* Handlebars
* EJS
实时通信
* Socket.io
API 客户端
* REST
* GraphQL
测试
* 单元测试框架
* Jest
* Mocha
* Chai
* 模拟测试(Mocking)
* Sinon
* Jasmine
软件库推荐
* Async.js
* PM2
* Commander.js
* Nodemailer</code></pre><h2>本月力荐</h2><ul><li>《人月神话》(小弗雷德里克)- 软件开发人员必读的软工圣经</li><li>《向上管理》(萧雨)- 职场人的必备技能</li><li>《单核工作法图解》(史蒂夫·诺特伯格)-高效工作方法论,带你解读单核工作法</li></ul><h2>关于我们</h2><p>公司:深圳有赞信息科技有限公司<br>地址:广东省深圳市南山区芒果网大厦11层<br>加入我们:joinus@youzan.com</p>
美业微前端的落地
https://segmentfault.com/a/1190000040106401
2021-06-02T14:31:48+08:00
2021-06-02T14:31:48+08:00
边城到此莫若
https://segmentfault.com/u/bianchengdaocimoruo
35
<p><a href="https://link.segmentfault.com/?enc=mZL9KPveDpk4HPTKgrXNOA%3D%3D.wU%2BqIlxORE5lY2ecXTim%2B7rbtba%2BH%2Bfxaq39m%2FBwHtWAtDFjrS%2BiaPxol2xQDSjlo3aA5xCBGHRW8iLcRvyQtiPLra8KQY0kLt1Td5xkgoW6tZ9oxq0Ogoye3fMy4pngnETA%2Bnx94rqEhXk8AfTaVRU8HwV%2FcfSEJhH0K0p%2B8Js%3D" rel="nofollow">Github 原链接</a></p><blockquote>2020年4月,有赞美业的前端团队历经7个月时间,完成了美业PC架构从单体SPA到微前端架构的设计、迁移工作。PPT在去年6月份就有了,现在再整理一下形成文章分享给大家。</blockquote><p><img src="/img/remote/1460000040106403" alt="头图" title="头图"></p><h2>目录</h2><ul><li><p><a href="#Part-01-%E2%80%9C%E5%A4%A7%E8%AF%9D%E2%80%9D%E5%BE%AE%E5%89%8D%E7%AB%AF">Part 01 “大话”微前端</a></p><ul><li><a href="#%E5%BE%AE%E5%89%8D%E7%AB%AF%E6%98%AF%E4%BB%80%E4%B9%88">微前端是什么</a></li><li><a href="#%E8%83%8C%E6%99%AF">背景</a></li><li><a href="#%E7%9B%AE%E6%A0%87">目标</a></li><li><a href="#%E8%BE%BE%E6%88%90%E4%BB%B7%E5%80%BC">达成价值</a></li><li><a href="#%E7%BC%BA%E7%82%B9">缺点</a></li></ul></li><li><p><a href="#Part-02-%E6%9E%B6%E6%9E%84%E4%B8%8E%E5%B7%A5%E7%A8%8B">Part 02 架构与工程</a></p><ul><li><a href="#%E5%BE%AE%E5%89%8D%E7%AB%AF%E6%96%B9%E6%A1%88%E6%9C%89%E5%93%AA%E4%BA%9B">微前端方案有哪些</a></li><li><a href="#%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E9%80%89%E5%9E%8B%E6%B3%A8%E6%84%8F%E7%82%B9">架构设计选型注意点</a></li><li><a href="#%E9%9C%80%E6%B1%82%E5%88%86%E6%9E%90">需求分析</a></li><li><a href="#%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99">设计原则</a></li><li><a href="#%E5%BA%94%E7%94%A8%E6%9E%B6%E6%9E%84%E5%9B%BE">应用架构图</a></li><li><a href="#%E7%B3%BB%E7%BB%9F%E6%8B%86%E5%88%86">系统拆分</a></li><li><a href="#%E6%97%B6%E5%BA%8F%E5%9B%BE">时序图</a></li><li><a href="#%E5%89%8D%E7%AB%AF%E6%B5%81%E7%A8%8B%E5%9B%BE">前端流程图</a></li></ul></li><li><p><a href="#Part-03-%E5%85%B3%E9%94%AE%E6%8A%80%E6%9C%AF">Part 03 关键技术</a></p><ul><li><a href="#%E5%85%B3%E9%94%AE%E6%8A%80%E6%9C%AF%E4%B8%80%E8%A7%88">关键技术一览</a></li><li><a href="#%5B%E6%9E%B6%E6%9E%84%E6%A0%B8%E5%BF%83%5D%E6%B6%88%E6%81%AF%E9%80%9A%E4%BF%A1">架构核心</a></li><li><a href="#%5B%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83%5DApollo">注册中心</a></li><li><a href="#%5B%E4%BB%A3%E7%A0%81%E5%A4%8D%E7%94%A8%5D%E5%AD%90%E5%BA%94%E7%94%A8%E4%B9%8B%E9%97%B4%E5%A6%82%E4%BD%95%E5%A4%8D%E7%94%A8%E5%85%AC%E5%85%B1%E5%BA%93">代码复用</a></li><li><a href="#%5B%E5%AD%90%E5%BA%94%E7%94%A8%5D%E5%AD%90%E5%BA%94%E7%94%A8%E5%A6%82%E4%BD%95%E6%8E%A5%E5%85%A5">子应用</a></li></ul></li><li><p><a href="#Part-04-%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%96%BD">Part 04 项目实施</a></p><ul><li><a href="#1.%E7%AB%8B%E9%A1%B9%E5%89%8D%E7%9A%84%E5%BF%83%E8%B7%AF">立项前的心路</a></li><li><a href="#2.%E5%8F%82%E8%80%83%E5%BE%AE%E5%89%8D%E7%AB%AF%E8%B5%84%E6%96%99">参考微前端资料</a></li><li><a href="#3.%E8%BF%9B%E8%A1%8CPC%E6%9E%B6%E6%9E%84%E4%BC%98%E5%8C%96%E8%AE%A1%E5%88%92">进行PC架构优化计划</a></li><li><a href="#4.%E9%A3%8E%E9%99%A9">风险</a></li><li><a href="#5.%E8%BF%AD%E4%BB%A3%E7%AB%8B%E9%A1%B9">迭代立项</a></li><li><a href="#6.%E8%BF%9B%E5%B1%95">进展</a></li><li><a href="#7.%E5%90%8E%E7%BB%AD%E8%AE%A1%E5%88%92">后续计划</a></li></ul></li></ul><h2>Part 01 “大话”微前端</h2><blockquote>把这个事情的前因后果讲清楚</blockquote><h3>微前端是什么</h3><p>想要回答这个问题直接给一个定义其实没那么难,但是没接触过的同学未必理解。所以需要先介绍一下背景,再解释会更容易明白。</p><p><img src="/img/remote/1460000040106404" alt="web发展1" title="web发展1"></p><p>这张图,展示了软件开发前端后分工的三个时期:</p><ol><li>单体应用:在软件开发初期和一些小型的Web网站架构中,前端后端数据库人员存在同一个团队,大家的代码资产也在同一个物理空间,随着项目的发展,我们的代码资产发展到一定程度就被变成了巨石。</li><li>前后端分离:前端和后端团队拆分,在软件架构上也有了分离,彼此依靠约定去协作,大家的生产资料开始有了物理上的隔离。</li><li>微服务化:后端团队按照实际业务进行了垂直领域的拆分单一后端系统的复杂度被得到分治,后端服务之间依靠远程调用去交互。这个时候前端需要去调用后端服务时候,就需要加入一层API网关或者BFF来进行接入。</li></ol><p><img src="/img/remote/1460000040106405" alt="web发展2" title="web发展2"></p><p>现在很多互联网公司的研发团队的工作模式更靠近这种,把整个产品拆分成多个阿米巴模式的业务小组。 <br>在这种研发流程和组织模式下,后端的架构已经通过微服务化形成了拆分可调整的形态,前端如果还处于单体应用模式,不谈其它,前端的架构已经给协作带来瓶颈。 <br>另外 Web 3.0 时代来临,前端应用越来越重,随着业务的发展迭代和项目代码的堆积,前端应用在勤劳的生产下演变成了一个庞然大物。人关注复杂度的能力有限,维度大概维持在5~8左右。单体应用聚合的生产资料太多,带来复杂性的维度太多,也容易引发更多的问题。简而言之,传统的SPA已经没办法很好的应对快速业务发展给技术底层的考验。 <br>我们的产品和前端项目也同样遇到了这个问题。如何解决这个问题呢? <br>其实后端的发展已经给出了可借鉴的方案,在理念上参照微服务/微内核的微前端架构应时而生。 <br>想要解决这个问题,在吸引力法则的指引下我们遇到了微前端架构,也验证了它的确帮助我们解决了这个难题。 </p><p>现在给出我们的微前端这样一种定义:</p><blockquote>微前端是一种类似于微内核的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单体应用转变为多个小型前端应用聚合为一的应用。多个前端应用还可以独立运行、独立开发、独立部署。</blockquote><h3>背景</h3><p><img src="/img/remote/1460000040106406" alt="背景" title="背景"></p><ol><li>美业PC作为一个单体应用经历4年迭代开发,代码量和依赖庞大,纯业务代码经统计有60多万行</li><li>工程方面,构建部署的速度极慢,开发人员本地调试体验差效率低,一次简单的构建+发布需要7+8=15分钟以上</li><li>代码方面,业务代码耦合严重,影响范围难以收敛,多次带来了“蝴蝶效应”式的的线上Bug和故障</li><li>技术方面,通用依赖升级带来的改动和回归成本巨大,涉及例如Zent组件、中台组件等依赖包相关的日常需求和技术升级几乎不可推动</li><li>测试方面,单应用应对多人和多项目发布,单应用发布总和高且非常频繁,每次的集成测试都有冲突处理和新问题暴露的风险</li><li>组织方面,单应用也无法很好应对业务小组的开发组织形式,边界职责不清晰且模块开发易干扰</li><li>架构方面,前端无法和后端形成对应的领域应用开发模式,不利于业务的下沉,也无法支持前端能力的服务化和对技术栈的演进依赖</li></ol><p>总体来说,臃肿的单体应用模式,给开发人员带来了无法忍受的难处,给快速支撑业务带来了很大的瓶颈,也没有信心应对接下来的业务的继续拓展。对美业PC进行架构调整就是非常迫切和有价值的事情了</p><h3>目标</h3><ol><li>业务架构层面,围绕美业PC的业务形态、项目架构以及发展趋势,将大型多团队协同开发的前端应用视为多个独立团队所产出功能的组合。</li><li>技术架构层面,解耦大型前端应用,拆分成基座应用、微前端内核、注册中心、若干独立开发部署的子系统,形成分布式体系的中心化治理系统。</li><li>软件工程方面,保证渐进式迁移和改造,保证新老应用的正常运行。</li></ol><h3>达成价值</h3><h4>业务价值</h4><ul><li>实现了前端为维度的产品的原子化,如果整合新业务,子应用可以快速被其他业务集成</li><li>以业务领域划分,让组织架构调整下的项目多人协作更职责清晰和成本低,且适应组织架构调整</li><li>减慢系统的熵增,铺平业务发展道路。</li></ul><h4>工程价值</h4><ul><li>实现了业务子应用独立开发和部署,构建部署的等待耗时从15分钟降到了1分半</li><li>支持渐进式架构,系统子应用之间依赖无关,可以单个升级依赖,技术栈允许不一致,技术迭代的空间更大</li><li>前端能力能够服务化输出</li><li>架构灵活,新的业务可以在不增加现存业务开发人员认知负担的前提下,自由生长无限拓展</li></ul><h3>缺点</h3><p>一个架构的设计其实对整体的一个权衡和取舍,除了价值和优势之外,也带来一些需要去考虑的影响。</p><p><img src="/img/remote/1460000040106407" alt="缺点" title="缺点"></p><h2>Part 02 架构与工程</h2><blockquote>从全局视角把握成果</blockquote><h3>微前端方案有哪些</h3><ol><li>使用 HTTP 服务器反向代理到多个应用</li><li>在不同的框架之上设计通讯、加载机制</li><li>通过组合多个独立应用、组件来构建一个单体应用</li><li>使用 iFrame 及自定义消息传递机制</li><li>使用纯 Web Components 构建应用</li><li>结合 Web Components 构建</li></ol><p><img src="/img/remote/1460000040106408" alt="微前端" title="微前端"></p><p>每种方案都有自己的优劣,我们兄弟团队采用了最原始的网关转发配置类似 Nginx 配置反向代理,从接入层的角度来将系统组合,但是每一次新增和调整都需要在运维层面去配置。 <br><img src="/img/remote/1460000040106409" alt="nginx方案" title="nginx方案"><br>而 iframe 嵌套是最简单和最快速的方案,但是 iframe的弊端也是无法避免的。 <br>Web Components的方案则需要大量的改造成本。 <br>组合式应用路由分发方案改造成本中等且满足大部分需求,也不影响个前端子应用的体验,是当时比较先进的一种方案。</p><h3>架构设计选型注意点</h3><ul><li>如何降低系统的复杂度?</li><li>如何保障系统的可维护性?</li><li>如何保障系统的可拓展性?</li><li>如何保障系统的可用性?</li><li>如何保障系统的性能?</li></ul><p>综合评估之后我们选用了组合式应用路由分发方案,但是仍然有架构整体蓝图和工程实现需要去设计。</p><h3>需求分析</h3><ol><li>子应用独立运行/部署</li><li>中心控制加载(服务发现/服务注册)</li><li>子应用公用部分复用</li><li>规范子应用的接入</li><li>基座应用路由和容器管理</li><li>建立配套基础设施</li></ol><h3>设计原则</h3><ol><li>支持渐进式迁移,平滑过渡</li><li>拆分原则统一,尝试领域划分来解耦</li></ol><h3>应用架构图</h3><p><img src="/img/remote/1460000040106410" alt="应用架构图" title="应用架构图"></p><h3>系统拆分</h3><p><img src="/img/remote/1460000040106411" alt="系统拆分" title="系统拆分"></p><p>这里拆分需要说明三个点:</p><ul><li>独立部署(服务注册):上传应用资源包(打包生成文件)到Apollo配置平台,是一个点睛之笔</li><li>服务化和npm包插件化的区别是不需要通过父应用构建来集成,彼此依赖无关,发布独立,更加灵活/可靠</li><li>同时 Apollo 承载了注册中心的功能,可以省去子应用的web服务器的这一层,简化了架构</li></ul><h3>时序图</h3><p><img src="/img/remote/1460000040106412" alt="时序图" title="时序图"></p><h3>前端流程图</h3><p><img src="/img/remote/1460000040106413" alt="流程图" title="流程图"></p><p>## Part 03 关键技术</p><blockquote>落地中有哪些值得一提的技术细节</blockquote><h3>关键技术一览</h3><p>我们按项目拆分来结构化讲述,有架构核心、注册中心、子应用、代码复用四篇。 <br>其中包含了这些技术点:</p><ol><li>Apollo</li><li>Apollo Cli</li><li>Version Manage</li><li>Sandbox</li><li>RouterMonitor</li><li>MicroPageLoader</li><li>Shared Menu</li><li>Shared Common</li></ol><h3>[架构核心]消息通信</h3><p><img src="/img/remote/1460000040106414" alt="消息通信" title="消息通信"></p><p><img src="/img/remote/1460000040106415" alt="消息通信1" title="消息通信1"></p><p><img src="/img/remote/1460000040106416" alt="消息通信2" title="消息通信2"></p><p><img src="/img/remote/1460000040106418" alt="消息通信3" title="消息通信3"></p><h3>[架构核心]路由分发</h3><p><img src="/img/remote/1460000040106419" alt="路由分发" title="路由分发"></p><p>当浏览器的路径变化后,最先接受到这个变化的是基座的router,全部的路由变化由基座路由 RouterMonitor 掌管,因为它会去劫持所有引起url变化的操作,从而获取路由切换的时机。如果是<code>apps/xxx/#</code>之前的变化,只会拦截阻止浏览器再次发起网页请求不会下发,没有涉及#之前的url变化就下发到子应用,让子应用路由接管。</p><h3>[架构核心]应用隔离</h3><p>主要分为 JavaScript执行环境隔离 和 CSS样式隔离。</p><p>JavaScript 执行环境隔离:每当子应用的JavaScript被加载并运行时,它的核心实际上是对全局对象 window 的修改以及一些全局事件的的改变,例如 JQuery 这个js运行之后,会在 window 上挂载一个 window.$ 对象,对于其他库 React、Vue 也不例外。为此,需要在加载和卸载每个子应用的同时,尽可能消除这种冲突和影响,最普遍的做法是采用沙箱机制 SandBox。 <br>沙箱机制的核心是让局部的 JavaScript 运行时,对外部对象的访问和修改处在可控的范围内,即无论内部怎么运行,都不会影响外部的对象。通常在 Node.js 端可以采用 vm 模块,而对于浏览器,则需要结合 with 关键字和 window.Proxy 对象来实现浏览器端的沙箱。</p><p>CSS 样式隔离:当基座应用、子应用同屏渲染时,就可能会有一些样式相互污染,如果要彻底隔离 CSS 污染,可以采用 CSS Module 或者命名空间的方式,给每个子应用模块以特定前缀,即可保证不会相互干扰,可以采用 webpack 的 postcss 插件,在打包时添加特定的前缀。 <br>对于子应用与子应用之间的CSS隔离就非常简单,在每次应用加载是,就将改应用所有的 link 和 style 内容进行标记。在应用卸载后,同步卸载页面上对应的 link 和 style 即可。</p><h3>[架构核心]核心流程图</h3><p>我们把路由分发、应用隔离、应用加载、通用业务逻辑收纳到到了微前端内核的二方包中,用作各个业务线复用,在内部达成统一约定。</p><p><img src="/img/remote/1460000040106420" alt="内核流程图" title="内核流程图"></p><h3>[注册中心]Apollo</h3><p>其实大部分公司在落地微前端方案的时候,并有没所谓的注册中心的概念。为什么我们的微前端也会有注册中心这个概念和实际存在呢?选型的思考点也主要来自我们后端的微服务架构。</p><p><img src="/img/remote/1460000040106421" alt="注册中心" title="注册中心"></p><h4>为什么选择引入注册中心增加整体架构的复杂度?</h4><p>两个原因:</p><ol><li>我们的子应用之间虽然不需要通信,但是也存在基座应用需要所有子应用的资源信息的情况,用来维护路由对应子应用资源地址的映射。大部分公司落地时候,都把子应用的地址信息硬编码到了基座。这样子应用增删改时候,就需要去重新部署基座应用,这违背了我们解耦的初衷。注册中心把这份映射文件从基座剥离出来了,让架构具备了更好的解耦和柔性。</li><li>要知道我们的子应用的产物入口是 hash 化的上传到 CDN 的 JS 文件,同时避免子应用发布也需要发布基座应用。有两个解决方案,一种是增加子应用的 Web 服务器,可以通过固定的 HTTP 服务地址拿到最新的静态资源文件。一种就是增加注册中心,子应用发布就是推送新的 JS地址给到 注册中心,子应用的架构就可以更薄。</li></ol><p>需要一个注册中心的话,我们也有两种方案,一种是自己自研一个专门服务于自己的微前端,虽然可以更加贴合和聚焦,但是作为注册中心,高可用的技术底层要求下的熔断降级等机制必不可少,这些研发难度大成本也高。还有一种是直接应用成熟的提供注册中心能力的开源项目或者依赖公司的已经存在的技术设施组件。</p><p>最后我们确定在选用公司内部的基础技术设施的 <a href="https://link.segmentfault.com/?enc=JRkyRt7OZNdBY47MkBUh9Q%3D%3D.Fqr7g%2BJBJtbmjdhVjK2%2BYl3e92syUoKRPI6k53dAjOHK0vVpNW%2BXpZbO%2Ba0Jo4Cm" rel="nofollow">Apollo</a> 项目,优势有这么两方面。</p><ol><li>项目本身开源,成熟程度很高,在多环境、即时性、版本管理、灰度发布、权限管理、开放API、支持端、简单部署等功能性方面做得很不错,是一个值得信赖的高可用的配置中心。</li><li>公司内部针对做了私有化定制和部署,更加适配业务,并且在 Java 和 Node 场景下都有稳定和使用,有维护人员值班。</li></ol><p><img src="/img/remote/1460000040106422" alt="apollo-basic" title="apollo-basic"></p><h4>子应用的打包构建体验</h4><ol><li>定位:一个子应用构建完是一个带 hash 的静态资源,等待被基座加载。</li><li><p>怎么做:</p><ol><li>打包一个单入口的静态资源,同时暴露全局方法给基座</li><li>每次构建生成带 hash 的入口 app.js</li><li>获取打包产出生成上传配置</li><li>根据环境参数上传到apollo</li></ol></li><li><p>体验如何</p><blockquote>非常轻量,无须发布,构建即可</blockquote></li></ol><h4>子应用如何推送打包完成的 cdn 地址给 Apollo</h4><p><img src="/img/remote/1460000040106423" alt="apollo页面" title="apollo页面"></p><ol><li>获取打包完成的产物的 JSON,获取入口文件 Hash,和当前项目的基础信息。</li><li>基于上述配置生成内容,然后调用 Apollo 平台开放的 API 上传到 Apollo。</li></ol><h4>如何进行多环境发布及服务链协作</h4><p><img src="/img/remote/1460000040106424" alt="微应用发布" title="微应用发布"></p><ol><li>环境主要分为测试、预发、生产。</li><li>打包完成后,根据微前端构建平台指定环境。</li><li>推送配置时候,指定 Apollo 对应的环境集群就好了。</li><li>基座应用在运行时候,会根据环境与 Apollo 交互对应环境集群的注册表信息。</li></ol><h3>[代码复用]子应用之间如何复用公共库</h3><p>1、添加 shared 为远程仓库</p><pre><code>git remote add shared http://gitlab.xxx-inc.com/xxx/xxx-pc-shared.git</code></pre><p>2、将 shared 添加到 report 项目中</p><pre><code>git subtree add --prefix=src/shared shared master</code></pre><p>3、拉取 shared 代码</p><pre><code>git subtree pull --prefix=src/shared shared master</code></pre><p>4、提交本地改动到 shared</p><pre><code>git subtree push --prefix=src/shared shared hotfix/xxx</code></pre><blockquote>注:如果是新创建子应用 1-2-3-4 ;如果是去修改一个子引用 1-3-4</blockquote><h3>[代码复用]使用shared需要注意什么</h3><ol><li>修改了 shared 的组件,需要 push 改动到 shared 仓库</li><li>如果一个 shared 中的组件被某个子应用频繁更新,可以考虑将这个组件从 shared 中移除,内化到子应用中</li></ol><h3>[子应用]子应用如何接入</h3><p>首先,我们需要明白我们对子应用的定位:</p><blockquote>一个子应用构建完后是一个带 hash 的静态资源,等待被基座加载,然后在中心渲染视图,同时拥有自己的子路由</blockquote><p>第一步,根据我们的模板新建一个仓库,并置入对应子应用的代码</p><p><img src="/img/remote/1460000040106425" alt="子应用目录结构" title="子应用目录结构"></p><p>第二步,接入shared以及修改一系列配置文件</p><p>第三步,进行开发所需要的转发配置</p><p>第四部,运行,并尝试打包部署</p><h3>[子应用]子应用能独立调式吗?怎么基座应用联调?</h3><ol><li>开启基座,端口和资源映射到本地再调式</li><li><a href="https://link.segmentfault.com/?enc=2VLyxgc3cv0vYkaNTZ8srA%3D%3D.oK4gKsXeUU5SO028S1XGTwIP%2BdZIiKLAVIdIIH%2FNJcCycas6CicIOShc4ofqJofo" rel="nofollow">Zan-proxy</a></li><li>本地 Nginx 转发</li></ol><h3>[子应用]子应用开发体验</h3><p><img src="/img/remote/1460000040106426" alt="开发体验" title="开发体验"></p><h2>Part 04 项目实施</h2><blockquote>一个问题从出现到被解决走过的曲折道路</blockquote><h3>1.立项前的心路</h3><ol start="0"><li>看过微前端这个概念,觉得花里胡哨,玩弄名词,强行造出新概念。</li><li>对项目的目前出现的问题有个大概感知(是个问题)</li><li>从业务出发利用现有知识背景思考解决手段(几乎无解)</li><li>回想了解过微前端架构的概念和场景,感受到两者有契合(人生若只如初见)</li><li>参考行业的解决方案印证,决定用微前端来脱掉膨胀的包袱(原来是该拆了)</li><li>首先把项目在前端架构优化理了一遍,输出架构图(项目整体上探路)</li><li>接下来梳理各个业务模块的依赖,看下有哪些(子应用分析)</li><li>大量和不同人的聊天、了解、讨论,获取支撑技术选型的信息(外界专家)</li><li>确定微前端架构在美业下的落地基本模型(架构基本)</li><li>进行概要技术设计(具象化)</li><li>明确迭代范围</li><li>技术评审</li><li>拉帮结伙/分工</li><li>kickoff</li><li>然而故事才刚刚开始…</li></ol><h3>2.参考微前端资料</h3><p><img src="/img/remote/1460000040106427" alt="微前端资料" title="微前端资料"></p><h3>3.进行PC架构优化计划</h3><p><img src="/img/remote/1460000040106428" alt="PC架构优化计划1" title="PC架构优化计划1"></p><p><img src="/img/remote/1460000040106429" alt="PC架构优化计划2" title="PC架构优化计划2"></p><p><img src="/img/remote/1460000040106430" alt="PC架构图" title="PC架构图"></p><h3>4.风险</h3><h4>预知</h4><ol><li>开发人员投入度不足</li><li>技术上的不确定性来更多工期风险</li><li>细节的技术实现需要打磨耗时超出预期</li><li>部分功能难以实现</li></ol><h4>意外</h4><ol><li>对项目架构理解不准确</li><li>任务拆分和边界理解不到位</li><li>测试人员投入不足</li><li>协作摩擦</li></ol><h3>5.迭代立项</h3><p><img src="/img/remote/1460000040106431" alt="kickoff" title="kickoff"></p><h3>6.进展</h3><ol><li>PC微前端基座应用已上线</li><li>PC数据拆分成子应用已上线</li><li>协调中台前端抽取了美业微前端内核</li><li>通用工具方法和枚举的可视化</li><li>搭配Apollo平台形成了前端子应用资源的注册中心</li><li>子应用接入文档输出</li><li>若干前端技术体系的优化</li></ol><h3>7.后续计划</h3><p><img src="/img/remote/1460000040106432" alt="afterplan" title="afterplan"></p>
react hooks源码深入浅出(二)
https://segmentfault.com/a/1190000039171753
2021-02-04T16:38:43+08:00
2021-02-04T16:38:43+08:00
了不起的小六先生
https://segmentfault.com/u/liaobuqidexiaoliuxiansheng
7
<ul><li><a href="https://segmentfault.com/a/1190000038431635">react hooks源码深入浅出(一)</a></li><li><a href="https://segmentfault.com/a/1190000039171753">react hooks源码深入浅出(二)</a></li></ul><blockquote>在第一篇文章里我们了解了初次渲染过程react内部的处理流程和执行机制,接下里继续看看在状态更新阶段react是怎么处理的</blockquote><p>现在触发demo中onclick事件,也就是执行setCount方法</p><h5>同样从两个基础hook出发</h5><ul><li>useState</li><li>useEffect</li></ul><h5>更新阶段核心流程</h5><p><img src="/img/remote/1460000039171756" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h5>useState</h5><p>在开始之前我们带着两个问题:</p><ol><li>执行setCount后,内部发生了什么?</li><li>如果多次执行setCount,它是怎么样取到最新的值的?</li></ol><h5>首先解答第一个问题</h5><p>在第一篇文章说到了,在mountState阶段会绑定一个叫<code>dispatchAction</code>的方法然后作为参数返回,这个方法在我们的demo中就是setCount方法,没有印象的看下下面的代码</p><pre><code class="javascript">function mountState(initialState) {
// 还记不记得这个熟悉的方法
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}</code></pre><p><strong>继续深入看下diapatchAction干了什么</strong></p><pre><code class="javascript">function dispatchAction(fiber, queue, action) {
var update = {
expirationTime: expirationTime,
action: action,
next: null
};
// 处理当前hook的queue队列
var pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// 进入调度环节
scheduleWork(fiber, expirationTime);
}
}</code></pre><p><strong>其实干了两件事</strong></p><ol><li>创建update节点,连接到当前hook(也就是useState)的queue后面(<code>这个queue忘记的伙伴可以翻回第一篇文章中看看</code>),这样每次调用dispatchAction都会在后面连接一个update节点,从而生成一个更新队列(<code>这个更新队列后面会详细讲</code>)</li><li>然后开始这一轮的scheduleWork调度(<code>关于调度做了什么详看这篇文章,因为内容非常多,这里不做过多说明:https://segmentfault.com/a/1190000020737020?utm_source=tag-newest</code>),大概流程就是将所有更新任务按照优先级排列,最后遍历整个fiberTree执行更新操作,更新阶会调用<code>beginWork</code>方法,这就又回到了我们初次渲染的流程,因为初次渲染时也会调用这个方法,就对应起来我们第一篇文章的初次渲染流程图</li></ol><p><strong>我们继续走</strong><br>按照上面的流程会走到这一步,又是熟悉的代码,此时我们会把<code>HooksDispatcherOnUpdateInDEV</code>赋值到<code>dispatcher</code>上</p><pre><code class="javascript"> {
// 首次执行currentDispatcher = null,所以进入else分支;在更新阶段会进入if分支
if (currentDispatcher !== null) {
currentDispatcher = HooksDispatcherOnUpdateInDEV;
} else {
currentDispatcher = HooksDispatcherOnMountInDEV;
}
}</code></pre><p><strong>继续看看<code>HooksDispatcherOnUpdateInDEV</code>是什么</strong></p><pre><code class="javascript">HooksDispatcherOnUpdateInDEV = {
useCallback: function (callback, deps) {
return updateCallback(callback, deps);
},
useEffect: function (create, deps) {
return updateEffect(create, deps);
},
useMemo: function (create, deps) {
return updateMemo(create, deps);
},
useState: function (initialState) {
return updateState(initialState);
}
}</code></pre><p><strong>发现在更新阶段遍历执行到useState时实际执行的是updateState方法,那继续看看updateState做了什么</strong></p><pre><code class="javascript">function updateState(initialState) {
return updateReducer(basicStateReducer);
}</code></pre><p><strong>继续看看updateReducer</strong></p><pre><code class="javascript">function updateReducer(reducer, initialArg, init) {
// 获取到当前hook,其实也就是直接.next就可以
var hook = updateWorkInProgressHook();
var queue = hook.queue;
// 取到待更新的队列
var pendingQueue = queue.pending;
// 如果待更新队列不为空,那么遍历处理
if (pendingQueue !== null) {
var first = pendingQueue.next;
var newState = null;
var update = first;
queue.pending = null;
// 循环遍历,是更新阶段的核心和关键,
do {
var action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
// 最新的状态值赋值给memoizedState
hook.memoizedState = newState;
}
// 将状态值和更新方法返回,就和初次渲染一样的流程
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}</code></pre><p><strong>上面是是核心流程代码,源码要比这个更加复杂和健全一些,我们这里不做过多涉及,其实主要做了两件事</strong></p><ol><li>获取当前hook的更新队列pendingQueue也就是上面通过queue连接起来的更新队列,举个形象的例子,比如我们执行了三次setCount方法,这个时候我们当前useState hook的queue队列中就会有三项</li><li>拿到我们的更新队列pendingQueue,<code>循环遍历</code>进行计算和赋值操作,最终会将最新的state值复制到hook的memorizedState上并返回</li></ol><p><strong>综上就是我们抛出的第一个问题的答案,接下来回答第二个问题,在多次setCount后是怎么获取到最新值的?</strong><br>所以综上我们知道了进行状态更新后方法执行顺序为<code>dispatchAction->updateReducer</code>, 我们把<code>dispatchAction</code>方法的核心代码拿出来,如下</p><pre><code class="javascript">// dispatchAction核心代码
var pending = queue.pending;
// 这里是链表创建和连接的核心
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;</code></pre><p>假设当前共执行了三次setCount,分别是setCount(1),setCount(2),setCount(3),拆开三次来看,当执行第一次setCount(1)时会执行下面的代码</p><pre><code class="javascript">// dispatchAction内
var pending = queue.pending;
if(pending == null){
update(1) = update(1).next;
queue.pending = update(1);
}
// updateReducer内,此时first是update(1)
first = queue.pending.next;</code></pre><p><img src="/img/remote/1460000039171757" alt="在这里插入图片描述" title="在这里插入图片描述"><br>执行完成后queue队列如上图所示,fisrt是用来记录最开始的update的一个节点,<code>此时就是update(1)</code>,先不用关心,继续执行setCount(2),同上</p><pre><code class="javascript">// 这里的pending其实也就是update(1)
var pending = queue.pending;
// 因为此时pending != null,所以代码走到else中
else{
update(2).next = pending.next;
pending.next = update(2);
}
queue.pending = update(2);
// 此时first仍是update(1)
first = queue.pendind.next;</code></pre><p><img src="/img/remote/1460000039171755" alt="在这里插入图片描述" title="在这里插入图片描述"><br>执行完setCount(2)后的queue队列如上,继续执行setCount(3)</p><pre><code class="javascript">// 这里的pending其实也就是update(2)
var pending = queue.pending;
// 因为此时pending != null,所以代码走到else中
else{
// 这一步很关键,结合上面的图,是把update(1)赋值到了update(3)的next上
update(3).next = pending.next;
// 因为此时pending是update(2),所以这一步就是把update(3)赋值到update(2)的next上
pending.next = update(3);
}
queue.pending = update(3);
// 此时first仍是update(1)
first = queue.pendind.next;</code></pre><p><img src="/img/remote/1460000039171758" alt="在这里插入图片描述" title="在这里插入图片描述">综上,执行完三次setCount后的queue队列为上图所示,接下来react内部会遍历queue队列(也就是update环形链表)<br>上面说过setCount后react内部方法执行顺序为<code>dispatchAction -> updateReducer</code>,现在开始执行<code>updateReducer</code>的遍历过程,根据核心代码</p><pre><code class="javascript">// updateReducer核心代码
var pendingQueue = queue.pending;
if (pendingQueue !== null) {
// first是update(1)
var first = pendingQueue.next;
var newState = null;
var update = first;
// 循环遍历,是更新阶段的核心和关键,
do {
var action = update.action;
// reducer其实就是判断我们传入的值是否为函数如果是的话执行函数放回新值;如果不是直接返回新值
newState = reducer(newState, action);
// 然后遍历下一个update
update = update.next;
} while (update !== null && update !== first);
// 最新的状态值赋值给memoizedState
hook.memoizedState = newState;
}</code></pre><p>一开始将update(1)赋值给update,然后获取newState也就是1,接下来<code>update=update.next</code>,此时update成了update(2),依次遍历,终止条件为<code>update === first</code>,也就是当<code>update = update(3)</code>时满足了终止条件,此时newState = 3,取到了最新值。<br>这样可以保证整个update链表都循环了一遍同时取到的是链表中的最后一个节点(也就是最新节点)<br>综上,解答了我们一开始抛出的第二个问题。</p><h5>useEffect</h5><p>同上,看到<code>HooksDispatcherOnUpdateInDEV</code>内部useEffect具体执行的是updateEffect</p><pre><code class="javascript">HooksDispatcherOnUpdateInDEV = {
useCallback: function (callback, deps) {
return updateCallback(callback, deps);
},
useEffect: function (create, deps) {
return updateEffect(create, deps);
},
useMemo: function (create, deps) {
return updateMemo(create, deps);
},
useState: function (initialState) {
return updateState(initialState);
}
}</code></pre><p>继续看updateEffect</p><pre><code class="javascript">function updateEffect(create, deps) {
{
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1);
}
return updateEffectImpl(Update | Passive, Passive$1, create, deps);
}</code></pre><p>继续看updateEffectImpl</p><pre><code class="javascript">function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 获取到当前hook
var hook = updateWorkInProgressHook();
// 比较依赖项是否发生了变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果相同则不对当前hook的属性进行更新
pushEffect(hookEffectTag, create, destroy, nextDeps);
return;
}
// 如果依赖项发生了变化,更新当前hook的memoizedState,这里的赋值只是做一个记录,并没有实际意义
currentlyRenderingFiber$1.effectTag |= fiberEffectTag;
hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, destroy, nextDeps);
}</code></pre><p>会发现无论useEffect的依赖项是否变化,都会执行pushEffect方法,那我们一探究竟</p><pre><code class="javascript">function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
next: null
};
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
// 创建/更新componentUpdateQueue队列
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}</code></pre><p>主要做了两件事情:<br>1、创建一个effect对象并返回(和初次渲染的流程相同)<br>2、同时创建/更新componentUpdateQueue队列,这个队列是用来专门存取当前组件中所有的useEffect这个hook的队列(<code>因为useEffect的回调其实是异步执行的,这里专门用一个队列存取是为了在调度阶段对所有的回调函数更方便的进行遍历处理</code>),componentUpdateQueue队列不存在的话会进行创建,如果存在,会和mountState阶段一样创建一个effect的循环链表(<code>这里就不画图了,具体参考上面的update更新队列的图片,只是把update替换成了effect</code>),每个effect对象中有一个tag属性(<code>tag的值类似于0和1</code>),刚才说到在调度阶段回遍历每一个effect,这个属性就是在遍历过程中用来判断useEffect回调是否需要被执行(<code>这里再推荐一个讲解useEffect执行时机的深度好文:https://www.cnblogs.com/iheyunfei/p/13065047.html</code>)</p><p><strong>到这里更新阶段两个核心hook的执行流程就讲解完毕</strong></p><hr><p><strong>以下是我们在使用hook过程中遇到的一些问题,也可以一一解答了</strong></p><blockquote>1、为什么hook之间一定要固定顺序/不能用条件判断?</blockquote><p>因为在某一组件中,每个hook之间是通过next指针依次按顺序连接的,所以一旦使用条件判断后会导致某个hook在某个情况下不存在,那么整个hook链表就被中断,无法正常遍历以及hook的获取,从而引发问题</p><blockquote>2、多次state的更新,是如何以最后一次为准的,内部机制是<br>怎样的?</blockquote><p>一句话陈述:通过一个update队列存储多次state的值依次遍历获取到最新值<br>具体参考上文的详细讲解</p><blockquote>3、useEffect如何实现仅执行单次/根据依赖项变动执行多次?</blockquote><p>初次遍历都会执行,更新阶段每个effect通过tag标识来判断是都需要执行回调</p><blockquote>4、hooks内部为什么使用链表结构而不使用其他数据结构实现?</blockquote><p>个人的看法是归结于链式结构和顺序结构的区别以及适用场景:<br>1、链式结构对内存要求不苛刻,可以随意存放<br>2、链式结构更适合数据的增/删操作,在分析源码过程发现增/删的场景偏多(update、effect循环队列的生成等)</p><p>如果本文对你有帮助,那就请大佬们点个小赞或者收藏,如若分析有误也请及时纠正。</p>
Vue 3 Virtual Dom Diff源码阅读
https://segmentfault.com/a/1190000038654183
2020-12-25T14:40:04+08:00
2020-12-25T14:40:04+08:00
丶Vin
https://segmentfault.com/u/_vin_5d884143cd4cb
33
<h2>前言</h2><p><code>Vue3</code>出来一段时间了,对<code>diff</code>算法进行了一波优化。<br>在阅读之前,最好需要了解一些<code>diff</code>算法的基础:<br>1、<code>vNode</code>是什么? <br>2、为什么需要使用<code>diff</code>算法?<br>传送门:<a href="https://link.segmentfault.com/?enc=%2B%2B%2BZtanm75T9ZJvTlFJ6Mg%3D%3D.t9D2dAJzkF6maYKq2tIiBxfdhnTXp46E5Oc7soChecEbFnm3PxeUFHDF%2B3NBgmiC" rel="nofollow">VNode - 源码版</a></p><p>本文主要分为三个部分:<br>一、<code>diff</code>算法的流程和思路<br>二、深入源码,看看具体的实现以及代码的优化<br>三、<code>React 16以下</code>和<code>Vue2</code>移动<code>dom</code>的方式,以及<code>Vue3 diff</code>的优化</p><h2>Vue3 diff 思路</h2><p><img src="/img/bVcRDAc" alt="image.png" title="image.png"><br>了解过<code>React</code>或者<code>Vue2</code>的小伙伴应该都知道,通常<code>diff</code>对比只有在拥有相同的父元素时,才会往下遍历。那现在假设他们的父节点是相同的,现在直接开始进行子节点们的比较。为了区分不同的场景下的思路,每一个部分都会举的不同的例子。</p><p>第一个例子在头尾遍历预处理时使用:<br><img src="/img/bVcRDA0" alt="image.png" title="image.png"></p><h4>预处理优化</h4><p>与<code>Vue2</code>的<strong>双向遍历</strong>不一样,先来看看下面这两组简单的节点对比,在<code>Vue3</code>中首先会进行<strong>头尾的单向遍历</strong>,进行预处理优化。</p><h4>1、从头开始遍历</h4><p>首先会遍历开始节点,判断新老的第一个节点是否是同一个节点,相同的话,执行<code>patch</code>方法更新差异,然后往下继续比较,否则<code>break</code>跳出。可以看到下图中,<code>A vs A</code>是一样的,然后去比较<code>B</code>,<code>B</code>也是相同的节点,再去比较<code>C vs F</code>,发现不一样了<br><img src="/img/bVcRDAh" alt="image.png" title="image.png"></p><h4>2、尾部开始遍历</h4><p>接着我们开始从后往前遍历,也是找相同的元素,<code>G vs G</code>一致,那么执行<code>patch</code>后往前对比,<code>F vs F</code>一致,一方遍历完毕,跳出循环。<br><img src="/img/bVcRDAw" alt="image.png" title="image.png"></p><h4>3、一方已经处理完毕</h4><p>根据上面的操作,目前新节点还剩下一个新增节点<code>C</code>,此时会去判断是否老节点已经遍历完毕,然后直接新增真实的dom节点<code>C</code>。<br><img src="/img/bVcRDBa" alt="image.png" title="image.png"><br>那如果是老节点还剩下一个多余节点(下图为新例子),则会去判断新节点是否遍历完成,下图的<code>I</code>节点则是要卸载。<br><img src="/img/bVcRDBc" alt="image.png" title="image.png"></p><hr><p>到了这一步,比较核心的场景还没有出现,如果运气好,可能到这里就结束了,那我们也不能全靠运气。剩下的一个场景是新老节点都还有多个子节点存在的情况。那接下来看看,<code>Vue3</code>是怎么做的。为了结合<code>move</code>、<code>新增</code>和<code>卸载</code>的操作,在这里引入另一个全新的例子,。<br><img src="/img/bVcRFnD" alt="image.png" title="image.png"></p><p>每次在对元素进行移动的时候,我们可以发现一个规律,如果想要移动的次数最少,就意味着需要有一部分元素是稳定不动的,那么究竟能够保持稳定不动的元素有一些什么规律呢?<br>可以看一下上面这个例子:<code>C H D E</code> vs <code>D E I C</code>,在比对的时候,凭着肉眼可以看出只需要将<code>C</code>进行移动到最后,然后卸载<code>H</code>,新增<code>I</code>就好了。<code>D E</code>可以保持不动,可以发现<code>D E</code>在新老节点中的顺序都是不变的,<code>D</code>在<code>E</code>的后面,<strong>下标处于递增状态</strong>。</p><pre><code>这里引入一个概念,叫最长递增子序列。
官方解释:在一个给定的数组中,找到一组递增的数值,并且长度尽可能的大。
有点比较难理解,那来看具体例子:
const arr = [10, 9, 2, 5, 3, 7, 101, 18]
=> [2, 3, 7, 18]
这一列数组就是arr的最长递增子序列,其实[2, 3, 7, 101]也是。
所以最长递增子序列符合三个要求:
1、子序列内的数值是递增的
2、子序列内数值的下标在原数组中是递增的
3、这个子序列是能够找到的最长的
但是我们一般会找到数值较小的那一组数列,因为他们可以增长的空间会更多。
</code></pre><p>那接下来的思路是:如果能找到老节点在新节点序列中顺序不变的节点们,就知道,哪一些节点不需要移动,然后只需要把不在这里的节点插入进来就可以了。<strong>因为最后要呈现出来的顺序是新节点的顺序,移动是只要老节点移动,所以只要老节点保持最长顺序不变,通过移动个别节点,就能够跟它保持一致。</strong>所以在此之前,先把所有节点都找到,再找对应的序列。最后其实要得到的则是这一个数组:<code>[2, 3, 新增 , 0]</code>。其实<code>diff</code>移动的思路已经清楚了,接下来就是看看怎么从代码逻辑中去实现这段逻辑了。<br><img src="/img/bVcRFpi" alt="image.png" title="image.png"></p><h4>4、<code>patch && unmount</code></h4><blockquote>通过上面的铺垫,得知了要找到这样一个数组<code>[2, 3, 新增, 0]</code>,不过因为数组的初始值是<code>0</code>,代表的是<code>新增</code>的意思,所以其他元素坐标顺延<code>+1</code>,<code>0</code>仅代表<code>新增</code>,最后也就是<code>[3, 4, 0, 1]</code>,可以看成第1位,第2位,第3位的意思。</blockquote><p>找到这个数组就很简单了,先初始化一个数组:<code>[0, 0, 0, 0]</code>,再遍历老节点,找到对应的新节点,然后加入到新节点对应的坐标上。<br>开始遍历了,在遍历过程中,会执行<code>patch</code>和<code>unmount</code>操作,如下图表格:</p><table><thead><tr><th>当前老坐标下标</th><th>当前找到的新节点坐标</th><th>新节点坐标下所对应的旧节点数组<strong>(初始值为0,代表新增,加进来坐标+1)</strong></th></tr></thead><tbody><tr><td>0</td><td>3</td><td>[0, 0, 0, 1]</td></tr><tr><td>1</td><td>无</td><td>卸载,执行unmount方法</td></tr><tr><td>2</td><td>0</td><td>[3, 0, 0, 1]</td></tr><tr><td>3</td><td>1</td><td>[3, 4, 0, 1]</td></tr></tbody></table><p>跟着上面的表格,可以看元素变化图,以及真实的dom节点做了什么操作:</p><p>1、遍历老节点,拿到第一个节点<code>C</code>,去新节点列表中找相同的节点,找到新节点中有<code>C</code>,在第三位,下标为<code>3</code>,于是在数组的第<code>3</code>位下标中,把当前老节点的下标加进去,由于前面说过,坐标都要+1,所以此时数组为<code>[0, 0, 0, 1]</code>,并且此时也会去执行<code>patch</code>方法,会将新旧节点的差异部分对齐,比如新旧<code>C</code>节点仅有<code>class</code>不一致,此时便会去执行更新<code>class</code>的方法。<br><img src="/img/bVcRIqE" alt="image.png" title="image.png"><br>2、遍历到第二位<code>H</code>,<code>H</code>在新节点中找不到,所以会直接执行<code>unmount</code>方法,去卸载<code>H</code>,此时真实dom也发生了变化。<br><img src="/img/bVcRIqK" alt="image.png" title="image.png"><br>3、遍历到第三位<code>D</code>,继续去新节点列表中找相同的节点<code>D</code>,下标为<code>0</code>,于是在数组的第<code>0</code>位下标中,把当前老节点的下标+1塞进数组,所以此时数组为<code>[3, 0, 0, 1]</code>,并且此时也会去执行<code>patch</code>方法。<br><img src="/img/bVcRIqF" alt="image.png" title="image.png"><br>4、遍历到第四位<code>E</code>,同理,在新节点中找到后把当前老节点的下标+1塞进数组,所以此时数组为<code>[3, 4, 0, 1]</code>,并且此时也会去执行<code>patch</code>方法。<br><img src="/img/bVcRIqL" alt="image.png" title="image.png"></p><p>遍历完后,最后得到了一个<code>[3, 4, 0, 1]</code>的数组,并且此时已经执行了有相同节点的<code>patch</code>方法和多余节点的<code>unmount</code>方法。<br>通过肉眼可以看到,它的最长增长子序列为<code>[3, 4]</code>(本文不做最长递增子序列求解,想了解求解,请移步<a href="https://segmentfault.com/a/1190000039838442">最长递增子序列求解传送门</a>)。<code>[3, 4]</code>的下标为<code>[0, 1]</code>,也就是说新节点的第0位<code>D</code>和第1位<code>E</code>不需要动。</p><h4>5、<code>move && mount</code></h4><p>它所对应的是第一个节点<code>D</code>和第二个节点<code>E</code>,所以这两个节点是不需要动的。<br>最后再遍历数组<code>[3, 4, 0, 1]</code>,如果当前的节点与在最长增长子序列中,则不移动,为<code>0</code>直接<code>新增</code>,剩下的则<code>move</code>到当前位置。接下来想深入了解的话,可以看一下第二部分的源码,<code>Vue3 diff</code>的这段源码还是比较清晰的。</p><h2>源码</h2><blockquote>源码文件路径:packages/runtime-core/src/renderer.ts<br>源码仓库地址:<a href="https://link.segmentfault.com/?enc=ardOOneQaZ8PiDsdV2fEhg%3D%3D.D5ZxHyT00nevXREfFiCFpQuI4c36x%2BtaJcXODthM7fQwUOD6S64es%2BBeb2F2WAqg" rel="nofollow">vue-next</a></blockquote><h3>patchChildren</h3><p>我们从<code>patchChildren</code>方法开始,进行子节点之间的比较。</p><pre><code>const patchChildren: PatchChildrenFn = () => {
// 获得当前新旧节点下的子节点们
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// fragment有两种类型的静态标记:子节点有key、子节点无key
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// 子节点全部或者部分有key
patchKeyedChildren()
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// 子节点没有key
patchUnkeyedChildren()
return
}
}
// 子节点有三种可能:文本节点、数组(至少一个子节点)、没有子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 匹配到当前是文本节点:卸载之前的节点,为其设置文本节点
unmountChildren()
hostSetElementText()
} else {
// old子节点是数组
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 现在(new)也是数组(至少一个子节点),直接full diff(调用patchKeyedChildren())
} else {
// 否则当前没有子节点,直接卸载当前所有的子节点
unmountChildren()
}
} else {
// old的子节点是文本或者没有
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 清空文本
hostSetElementText(container, '')
}
// 现在(new)的节点是多个子节点,直接新增
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新建子节点
mountChildren()
}
}
}
}</code></pre><p>我们可以直接用文本描述一下这段代码:<br>1、获得当前新旧节点下的子节点们(c1、c2);<br>2、使用<code>patchFlag</code>进行按位与判断<code>fragment</code>的子节点是否有key(patchFlag是什么稍后下面说);<br>3、不管有没有key,只要匹配成功一定是数组,有key/部分有key则调用<code>patchKeyedChildren</code>方法进行diff计算,无key则调用<code>patchUnkeyedChildren</code>方法;<br>4、不是fragment节点,那么子节点有三种可能:文本节点、数组(至少一个子节点)、没有子节点;<br>5、如果new的子节点是<strong>文本节点</strong>:old有子节点的话则直接进行卸载,并为其设置文本节点;<br>6、否则new的子节点是<strong>数组 or 无节点</strong>,在这个基础上:</p><blockquote>如果old的子节点为数组,那么new的子节点也是数组的话,调用<code>patchKeyedChildren</code>方法,直接full diff,否则new没有子节点,直接进行卸载。<br>最后old的子节点为文本节点 or 没有节点(此时新节点可能为数组,也可能没有节点),所以当old的子节点为文本节点,那么则清空文本,new节点如果是数组的话,直接新增。</blockquote><p>7、此时所有的情况已经处理完毕了,不过真正的diff还没开始,那我们来看一下没有<code>key</code>的情况下,是如何进行<code>diff</code>的。</p><h3>patchUnkeyedChildren</h3><p>没有key的处理比较简单,直接上删减版源码</p><pre><code>const patchUnkeyedChildren = () => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
// 拿到新旧节点的最小长度
const commonLength = Math.min(oldLength, newLength)
let i
// 遍历新旧节点,进行patch
for (i = 0; i < commonLength; i++) {
// 如果新节点已经挂载过了(已经过了各种处理),则直接clone一份,否则创建一个新的vnode节点
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch()
}
// 如果旧节点的数量大于新节点数量
if (oldLength > newLength) {
// 直接卸载多余的节点
unmountChildren( )
} else {
// old length < new length => 直接进行创建
mountChildren()
}
}</code></pre><p>我们继续文本描述一下逻辑:<br>1、首先会拿到新旧节点的最短公共长度<br>2、然后遍历公共部分,直接进行patch<br>3、如果旧节点的数量大于新节点数量,直接卸载多余的节点,否则新建节点</p><h3>patchKeyedChildren</h3><p>到了Diff算法比较核心的部分,我们先看一个大概预览,了解一下流程~再把<code>patchKeyedChildren</code>源码内部拆分一下,逐步来看。</p><pre><code> const patchKeyedChildren = () => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index
// 1. 进行头部遍历,遇到相同节点则继续,不同节点则跳出循环
while (i <= e1 && i <= e2) {}
// 2. 进行尾部遍历,遇到相同节点则继续,不同节点则跳出循环
while (i <= e1 && i <= e2) {}
// 3. 如果旧节点已遍历完毕,并且新节点还有剩余,则遍历剩下的进行新增
if (i > e1) {
if (i <= e2) {}
}
// 4. 如果新节点已遍历完毕,并且旧节点还有剩余,则直接卸载
else if (i > e2) {
while (i <= e1) {}
}
// 5. 新旧节点都存在未遍历完的情况
else {
// 5.1 创建一个map,为剩余的新节点存储键值对,映射关系:key => index
// 5.2 遍历剩下的旧节点,新旧数据对比,移除不使用的旧节点
// 5.3 拿到最长递增子序列进行move or 新增挂载
}
}</code></pre><p><strong>1、第一步是进行头部遍历,遇到相同节点则继续,下标 + 1,不同节点则跳出循环</strong></p><pre><code> // 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
const n1 = c1[i]
// 如果新节点已经挂载过了(已经经历了各种处理),则直接clone一份,否则创建一个新的vnode节点
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
// 相同节点,则继续执行patch方法
if (isSameVNodeType(n1, n2)) {
patch()
} else {
break
}
i++
}
</code></pre><p><img src="/img/bVcRFrX" alt="image.png" title="image.png"></p><blockquote>此时i = 2, e1 = 6, e2 = 7, 旧节点剩下C、D、E、F、G,新节点剩下D、E、I、C、F、G</blockquote><p>这里判断是否为相同节点的方法<code>isSameVNodeType</code>,是通过类型和key来进行判断,在Vue2中是通过key和sel(属性选择器:tag + id + class)来判断是否是相同元素。这里的类型指的是ShapeFlag,也是一个标志位,是对元素的类型进行不同的分类,比如:元素、组件、fragment、插槽等等</p><pre><code>export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}</code></pre><p><strong>2、第二步是进行尾部遍历,遇到相同节点则继续,length - 1,不同节点则跳出循环</strong></p><pre><code> // 2. sync from end
// a (b c)
// d e (b c)
// 进行尾部遍历,遇到相同节点则继续,不同节点则跳出循环
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch()
} else {
break
}
e1--
e2--
}</code></pre><p><img src="/img/bVcRFrZ" alt="image.png" title="image.png"></p><blockquote>此时i = 2, e1 = 4, e2 = 5, 旧节点剩下C、D、E,新节点剩下D、E、I、C</blockquote><p><strong>3、如果旧节点已遍历完毕,并且新节点还有剩余,则遍历剩下的进行新增</strong></p><pre><code> // 3.common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(null, c2[i]) // 节点新增(伪代码)
i++
}
}
}</code></pre><p>因为我们上面的图例(i < e1)走不到这段逻辑,所以我们可以直接看一下代码注释(注释真的写得非常详细了,<code>patchKeyedChildren</code>里面的原注释我都保留了)。如果旧节点遍历完毕,开头或者尾部还剩下了新节点,则进行节点新增(通过传参,<code>patch</code>内部会处理)。</p><p><strong>4、如果新节点已经遍历完毕,则说明多余的节点需要卸载</strong></p><pre><code> // 4.common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}</code></pre><p>因为我们上面的图例(i < e2)依然走不到这段逻辑,所以我们可以继续看一下原注释。i > e2意味着新节点遍历完毕,如果新节点遍历完毕,开头或者尾部还剩下了旧节点,则进行节点卸载<code>unmount</code>。</p><p><strong>5、新旧节点都没有遍历完成的情况</strong></p><pre><code> // 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
const s1 = i // prev starting index
const s2 = i // next starting index
...
}</code></pre><p>按照上面图的例子来看,s1 = 2, s2 = 2,旧节点剩下C、D、E,新节点剩下D、E、I、C需要继续进行diff</p><p><strong>5.1、生成map对象,通过键值对的方式存储新节点的key => index</strong></p><pre><code> // 5.1 build key:index map for newChildren
// 创建一个空的map对象
const keyToNewIndexMap = new Map()
// 遍历剩下没有patch的新节点,也就是D、E、I、H
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
// 如果剩余的新节点有key的话,则将其存储起来,key对应index
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}</code></pre><p>执行完上面的方法,得到<code>keyToNewIndexMap = {D => 2, E => 3, I => 4, C => 5}</code>,<code>keyToNewIndexMap</code>主要用来干嘛呢~请继续往下看</p><p><strong>5.2、遍历剩下的旧节点,新旧数据对比,移除不使用的旧节点</strong></p><pre><code> // 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
let j
// 记录即将被patch过的新节点数量
let patched = 0
// 拿到剩下要遍历的新节点的长度,按照上面的图示toBePatched = 4
const toBePatched = e2 - s2 + 1
// 是否发生过移动
let moved = false
// 用于跟踪是否有任何节点移动
let maxNewIndexSoFar = 0
// works as Map<newIndex, oldIndex>
// 注意:旧节点 oldIndex偏移量 + 1
// 并且oldIndex = 0是一个特殊值,代表新节点没有对应的旧节点
// newIndexToOldIndexMap主要作用于最长增长子序列
// newIndexToOldIndexMap从变量名可以看出,它代表的是新旧节点的对应关系
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
// 此时newIndexToOldIndexMap = [0, 0, 0, 0]
// 遍历剩余旧节点的长度
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
if (patched >= toBePatched) {
// patched大于剩余新节点的长度时,代表当前所有新节点已经patch了,因此剩下的节点只能卸载
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
// 旧节点的key存在的话,则通过旧节点的key找到对应的新节点的index位置下标
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// 旧节点没有key的话,则遍历所有的新节点
for (j = s2; j <= e2; j++) {
// newIndexToOldIndexMap[j - s2]如果等于0的话
// 代表当前新节点还没有被patch,因为在下面的运算中
// 如果找到新节点对应的旧节点位置,newIndexToOldIndexMap[j - s2]则会等于旧节点的下标 + 1
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
// 当前新节点还没有被找到,并新旧节点相同,则将新节点的位置赋予newIndex
newIndex = j
break
}
}
}
if (newIndex === undefined) {
// 当前旧节点没有找到对应的新节点,则进行卸载
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 找到了对应的新节点,则将旧节点的位置存储在对应的新节点下标
newIndexToOldIndexMap[newIndex - s2] = i + 1
// maxNewIndexSoFar如果不是逐级递增,则代表有新节点的位置前移了,那么需要进行移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
// 更新节点差异
patch()
// 找到一个对应的新节点,+1
patched++
}
}</code></pre><p>这段代码比较长,但是总的来说做了下面几件事:<br>1、拿到新节点对应的旧节点下标<code>newIndexToOldIndexMap</code>(下标+1,因为0代表的是新节点没有对应的旧节点,直接创建新节点),在我们的图例中<code>newIndexToOldIndexMap = [4, 5, 0, 3]</code>。</p><p>2、存在在遍历的过程中,如果老节点找到对应的新节点,则进行打补丁,更新节点差异,找不到则删除该老节点</p><p>3️、通过新节点下标的顺序是否递增来判断,是否有节点发生过移动</p><p><strong>5.3、对剩下没有找到的新节点进行挂载,对需要移动的节点进行移动</strong></p><pre><code> // 5.3 move and mount
// 仅在有节点需要移动的时候才生成最长递增子序列
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// 此时图示中的increasingNewIndexSequence = [4, 5]
// 从后面开始遍历,将最后一个patch的节点用作锚点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
// 代表新增
if (newIndexToOldIndexMap[i] === 0) {
// mount new
patch( )
} else if (moved) {
// 移动的条件:当前最长子序列的length小于0(没有稳定的子序列),或者当前的节点不在稳定的序列中
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}</code></pre><p>最后这段源码用到了一个优化方法,最长上升子序列,这段大致的流程就是:<br>1、通过<code>moved</code>来判断当前是否有节点进行了移动,如果有的话则通过<code>getSequence(newIndexToOldIndexMap)</code>拿到最长上升子序列,我们的图示中拿到的是<code>increasingNewIndexSequence = [4, 5]</code>;</p><p>2、遍历剩余新节点的长度,从后面开始遍历,判断<code>newIndexToOldIndexMap[i] === 0</code>,当前的新节点是否有对应的老节点,如果等于0,就是没有,直接新增;</p><p>3、否则通过<code>moved</code>判断是否有移动,有移动的话,如果当前最长子序列的<code>length < 0</code>,或者当前的节点不在稳定的序列中,则意味着现在没有稳定的子序列,每个节点需要进行移动,或者,最后一个新节点,不在末尾的子序列中,子序列的末尾另有他人,那当前也需要进行移动。若是不符合移动的条件,则说明当前新节点在最长上升子序列中,不需要进行移动,只用等待别的节点去移动。</p><p>到这里,diff算法源码核心流程就了解得差不多了,接下来看看<code>Vue3</code>对比<code>React/Vue2</code>的优化点。</p><h2>React、Vue2、Vue3</h2><p>这里可以大体回顾一下<code>React</code>和<code>Vue2</code>移动dom的思路,用两个比较典型的例子进行介绍,这里主要对比的是,相同父元素的子元素之间的<code>diff</code>移动操作,所以在下面的例子中,都是在同一层面下,<code>A、B、C、D</code>为各自的<code>key</code>:<br><img src="/img/bVcRDCf" alt="image.png" title="image.png"></p><h4><strong><code>React 16以下的版本</code></strong></h4><blockquote><code>React</code>在移动dom节点时,是只会往右边进行移动的<br>从新节点开始遍历,在老节点中找相同的节点</blockquote><p><em>左边:普遍情况</em><br>1、<code>B</code>:在老节点中的第二位找到了相同的元素,由于上面说过,<code>React</code>只会往右移动,它会用一个变量记录所有找到的老节点的下标,这个变量叫做<code>lastIndex</code>,初始值为0,每找到一个比<code>lastIndex</code>大的老节点下标都会更新它,所以它永远是<strong>找到过的老节点下标的最大的那个值</strong>。并且每次找到的老节点的下标一定要小于lastIndex才可以往右移动,这句话这么理解,因为我们是要往右边进行移动的,往右边走是增大的,所以每次找到的老节点下标,如果比上次找到的老节点下标大的话,那就说明位置顺序是正常的,没有往右边移动的必要了。</p><p>那么此时,因为<code>lastIndex</code>的初始值是<code>0</code>,找到的老节点的下标为<code>1</code>,则无需移动,因为找到的老节点已经比较靠右了,但需要更新<code>lastIndex</code>。<br><img src="/img/bVcRJnL" alt="image.png" title="image.png"></p><p>2、<code>A</code>:上次记录的<code>lastIndex</code>已经更新到了<code>1</code>,此时找到的老节点<code>A</code>的位置为<code>0</code>,则需要进行右移。无需更新<code>lastIndex</code>。<br><img src="/img/bVcRJnN" alt="image.png" title="image.png"></p><p>3、<code>D</code>:当前<code>lastIndex</code>还是<code>1</code>,此时找到的老节点<code>D</code>的位置为<code>3</code>,是大于上次找到的老节点的最大位置的,所以无需移动。<code>lastIndex</code>更新为<code>3</code>。<br><img src="/img/bVcRJnO" alt="image.png" title="image.png"></p><p>4、<code>C</code>:<code>lastIndex</code>更新到了<code>3</code>,此时找到的老节点<code>C</code>的位置为<code>2</code>,因为实际上新节点中<code>C</code>是靠右的,不能比上次找到的老节点位置小,所以<code>C</code>需要移动。<br><img src="/img/bVcRJnQ" alt="image.png" title="image.png"></p><p>我们可以看到,在这种情况下,<code>真实dom</code>是<code>move</code>了两次,<code>patch</code>了四次。而实际上,这种移动次数也是最少的。但是往右移动这种方式,仔细想想,好像并不是所有场景下都那么完美的。比如,上面我们提到的右边的那种情况。</p><p><em>右边:极端情况</em><br><img src="/img/bVcRE4K" alt="image.png" title="image.png"></p><p>通过我们上面方法的比对,来看看移动路径:<br>1、<code>D</code>:由于找到的老节点在最右边,已经为最右边了,不能往右边移动了,所以暂定不动。<br><img src="/img/bVcRJn8" alt="image.png" title="image.png"></p><p>2、<code>A</code>:移动到最右边。<br><img src="/img/bVcRJoa" alt="image.png" title="image.png"></p><p>3、<code>B</code>:移动到最右边。<br><img src="/img/bVcRJoe" alt="image.png" title="image.png"></p><p>4、<code>C</code>:移动到最右边。<br><img src="/img/bVcRJof" alt="image.png" title="image.png"></p><p>明眼人都可以看出来,其实最好的移动方式只要移动一次就能够达到我们想要的效果,但是<code>React</code>需要移动三次。</p><h4><strong><code>Vue2</code></strong></h4><p>刚刚在<code>React</code>中右边例子的情况还有优化空间,那可以看看<code>Vue2</code>是怎么解决这个问题的。</p><blockquote><code>Vue2</code>为双向遍历,同时从前后往中间进行遍历<br>遍历的顺序:新头 vs 旧头、新尾 vs 旧尾、旧头 vs 新尾、新头 vs 旧尾</blockquote><p><em>右边:极端情况</em><br>1、按照上面说的遍历的顺序,先是进行<code>新旧节点当前首位A vs D</code>进行diff,不相同让<code>新旧节点当前末位 D vs C</code>进行diff,不相同继续让<code>旧节点当前首位和新节点当前末位 A vs C</code>进行diff,不相同继续让<code>新节点当前首位和旧节点当前末位 D vs D</code>进行diff,哦豁,皇天不负有心人,第四次寻找终于找到了,因为新旧节点的位置是一前一后,显然不应该,于是找到<code>D</code>后将它挪到前面来。之后两边的指针各挪一步。<br><img src="/img/bVcRFbD" alt="image.png" title="image.png"></p><p>2、紧接着,重复进行<code>新头 vs 旧头、新尾 vs 旧尾、旧头 vs 新尾、新头 vs 旧尾</code>这个遍历顺序,但是第一次就找到了<code>A vs A</code>,他们都是第一个,不需要移动,然后指针+1。<br><img src="/img/bVcRFbE" alt="image.png" title="image.png"></p><p>3、继续重复进行<code>新头 vs 旧头、新尾 vs 旧尾、旧头 vs 新尾、新头 vs 旧尾</code>这个遍历顺序,找到了<code>B vs B</code>,他们都是第一个,不需要移动,然后指针+1。<br><img src="/img/bVcRFbR" alt="image.png" title="image.png"></p><p>4、继续重复进行<code>新头 vs 旧头、新尾 vs 旧尾、旧头 vs 新尾、新头 vs 旧尾</code>这个遍历顺序,找到了<code>C vs C</code>,他们都是第一个,不需要移动,然后指针+1。<br><img src="/img/bVcRFbW" alt="image.png" title="image.png"></p><p>最后的结果是,所有的都遍历完了,最后只是将<code>D</code>挪到了第一位而已,对上面的情况进行了优化。当然,这是一个非常理想的状态,那接下来来看看,普遍的情况是怎么做的。</p><p><em>左边:普遍情况</em><br>前面我们说过了那个遍历的顺序:<code>新头 vs 旧头、新尾 vs 旧尾、旧头 vs 新尾、新头 vs 旧尾</code>,但是总不能每次都能够在最两边找到元素,接下来就来看看,如果前面的规则走完之后该怎么办。</p><p>1、前面的四次判断没有发现相同的新节点的话,则会遍历老节点找当前首位新节点<code>B</code>(<code>箭头指向的第一位新节点</code>),遍历老节点发现了<code>B</code>在第二位,那我们现在才遍历到第一位,显然与我们想要的位置不符,于是将<code>B的真实dom</code>移动到第一位,并将老节点列表原本<code>B</code>的位置设置为<code>undefined</code>,之后遇到<code>undefined</code>的元素一律跳过。然后新节点初始下标+1。<br><img src="/img/bVcRFgJ" alt="image.png" title="image.png"></p><p>2、新节点的下标+1后,我们的循环又重新开始了,又会按照前面的顺序进行判断(<code>新头 vs 旧头、新尾 vs 旧尾、旧头 vs 新尾、新头 vs 旧尾</code>),于是遍历到了<code>A vs A</code>,新旧节点的指针往后增加一位。<br><img src="/img/bVcRFgN" alt="image.png" title="image.png"><br>3、由于前面我们说过,<code>B</code>已经是<code>undefined</code>了,所以会跳过该元素,直接到<code>C</code>,新旧节点的<code>C</code>分别是末位和首位,于是移动<code>C的真实dom</code>到最后一位。旧节点首位指针+1,新节点末位指针-1。<br><img src="/img/bVcRFgO" alt="image.png" title="image.png"><br>4、最后只剩下了<code>D</code>元素,也就不需要移动了。<br><img src="/img/bVcRFgS" alt="image.png" title="image.png"></p><p><code>Vue2</code>在移动dom的上面已经做的比较好了,并且只会移动必须要挪动的那部分元素。通过前面的了解,<code>Vue3</code>好像并没有在<code>移动dom</code>上有更多的优化,那<code>Vue3</code>在diff的过程中优化了什么呢?</p><h4><strong><code>优化</code></strong></h4><p><strong>1、预处理优化</strong><br>平时我们在修改列表的时候,有一些比较常见场景,比如说列表中间节点的增加、删除、修改等,如果使用了这样的方式查找,可以<code>减少diff的时间</code>,甚至可以<code>不用diff</code>来达到我们想要的结果,并且还可以减少后续diff的复杂度。这个预处理优化策略,是<a href="https://link.segmentfault.com/?enc=wj3uHiJ2qTcjm8G4ZhYbCw%3D%3D.8G8jc5BlYafHTczeIj7sQ4QDYprG6y4n2puQ1g0Vh30au%2FNer9TA2DouuAwnJjom" rel="nofollow">Neil Fraser</a>提出的。<br>举个栗子:<br>像下面这些情况,在预处理中能够很快的做完这些处理。但是在<code>Vue2</code>中,会需要一个接一个的重复<code>新头 vs 旧头、新尾 vs 旧尾、旧头 vs 新尾、新头 vs 旧尾</code>。当然,<code>Vue3</code>也不是在所有场景中都是最优的。<br><img src="/img/bVcRFrf" alt="image.png" title="image.png"></p><p><strong>2、PatchFlags静态节点优化</strong><br><code>Vue2</code>在<code>patch</code>阶段时会进行全量<code>diff</code>,但是有的节点只声明了动态文本或者动态class,明明可以知道他是不变的,为什么还需要去diff它呢?所以<code>Vue3</code>在这个过程中做了一些优化,我们来看看这个叫做<code>PatchFlags</code>的东西是什么(只截了部分代码)。</p><pre><code>export const enum PatchFlags {
// 动态文本
TEXT = 1,
// 动态class
CLASS = 1 << 1,
// 动态style
STYLE = 1 << 2,
// 动态props
PROPS = 1 << 3,
// 动态变化的属性,比如:[attr] = "foo"
FULL_PROPS = 1 << 4,
...
}</code></pre><p>那翻译一下,其实这个枚举是长这样的:</p><pre><code>export const enum PatchFlags {
// 动态文本
// 十进制: 1
// 二进制: 0000 0001
TEXT = 1,
// 动态class
// 十进制: 1
// 二进制: 1往左移一位: 0000 0001
CLASS = 1 << 1,
// 动态style
// 十进制: 1
// 二进制: 1往左移两位: 0000 0010
STYLE = 1 << 2,
// 动态props
// 十进制: 1
// 二进制:1往左移三位: 0000 0100
PROPS = 1 << 3,
...
}</code></pre><p>举个例子:<br><img src="/img/bVcRIt1" alt="image.png" title="image.png"><br>这个<code>div</code>,在编译的过程中,会执行这样一段代码(伪代码):</p><pre><code>// 有动态绑定的class将执行
if (hasClassBinding) {
patchFlag |= PatchFlags.CLASS
}
// 有动态绑定的style将执行
if (hasStyleBinding) {
patchFlag |= PatchFlags.STYLE
}
// 有动态文本节点将执行
if (hasDynamicTextChild) {
patchFlag |= PatchFlags.TEXT
}</code></pre><p>因为上面的例子中只有动态class和style,没有动态文本节点,所以只会执行:</p><pre><code>patchFlag |= PatchFlags.CLASS
patchFlag |= PatchFlags.STYLE </code></pre><p><code>patchFlag |= PatchFlags.CLASS</code>执行过程:</p><pre><code>// 因为patchFlag初始值为0,所以第一次执行或运算时:
// 按位或:二进制位进行比对计算,有一个是1,结果的对应位就是1。
// 0000 0000
// 0000 0010
// ---------
// 0000 0010</code></pre><p>那么此时<code>二进制10等于十进制2,所以patchFlag = 2</code>。<br>紧接着执行<code>patchFlag |= PatchFlags.STYLE</code>:</p><pre><code>// 因为patchFlag此时为2,所以执行或运算时使用二进制10:
// 0000 0010
// 0000 0100
// ---------
// 0000 0110</code></pre><p>那么此时<code>二进制110等于十进制6,所以patchFlag = 6</code>。</p><p>然后在diff阶段执行<code>patch</code>方法的时候会将<code>patchFlag</code>取出来然后执行这一段代码,这段代码意味着,只有当括号内计算为<code>真</code>的时候才会执行<code>if</code>内的方法:<br><img src="/img/bVcRIuy" alt="image.png" title="image.png"></p><p>首先会判断<code>patchFlag & PatchFlags.TEXT</code>是否为真,这一次是会进行与运算,执行过程:</p><pre><code>// 按位与:二进制位进行比对计算,两个都是1,结果的对应位就才是1。
// 因为patchFlag此时等于6,所以第一次执行或运算时:
// 0000 0110 ----当前patchFlag
// 0000 0001 ----常量PatchFlags.TEXT
// ---------
// 0000 0000</code></pre><p>换算之后的结果等于<code>0</code>,所以不会执行修改文本节点的方法。当然,在我们的例子中文本节点是静态节点,也无需去比对修改。<br>接着会判断<code>patchFlag & PatchFlags.CLASS</code>:</p><pre><code>// 按位与:二进制位进行比对计算,两个都是1,结果的对应位就才是1。
// 因为patchFlag此时等于6,所以第一次执行或运算时:
// 0000 0110 ----当前patchFlag
// 0000 0010 ----常量PatchFlags.CLASS
// ---------
// 0000 0010</code></pre><p>此时十进制结果为<code>2</code>,则认为是动态节点,需要去执行修改<code>class</code>的方法,之后再判断<code>patchFlag & PatchFlags.STYLE</code>:</p><pre><code>// 按位与:二进制位进行比对计算,两个都是1,结果的对应位就才是1。
// 因为patchFlag此时等于6,所以第一次执行或运算时:
// 0000 0110 ----当前patchFlag
// 0000 0100 ----常量PatchFlags.STYLE
// ---------
// 0000 0100</code></pre><p>同理,也会执行修改style的方法。</p><p>当然<code>Vue3</code>做的优化不止这么多,大家可以多看看源码,会有很多代码优化上的收获。</p><blockquote>参考资料:<br>源码:<a href="https://link.segmentfault.com/?enc=K22TK9V8Nb%2FmHrKvfOlKSg%3D%3D.UXm%2FiCSyN7zpa4rKsvF9nvB0qaYEtQdceSDLar7OPWQ1CjPjkjsTiceeH4rJwUoY" rel="nofollow">https://github.com/vuejs/vue-...</a><br>diff优化策略:<a href="https://link.segmentfault.com/?enc=6GZJV9W7cb0v7zyZi2%2FvJQ%3D%3D.f1XgWHygMNNHF5O3ZJ%2Boqf0PeIXmwD%2BBcK0ED6hpLu6H4fIlh4ZZW0CgGN4W2WWj" rel="nofollow">https://neil.fraser.name/writ...</a><br>inforno:<a href="https://link.segmentfault.com/?enc=X1Jjf8YsW4uQ7i5sCqgKvA%3D%3D.Sn0G7%2Bh39y%2BuPXOKdhJx8J1XH7FsRRH9hE7uHDiAdKh88Pxs6App4ISaC%2BYQeJB6" rel="nofollow">https://github.com/infernojs/inferno</a><br><a href="https://link.segmentfault.com/?enc=hMq1MF68bOPYRDNQYEBhLg%3D%3D.OPIMMYc45lzBtAU%2FT9mXoqUu0L%2B%2FzidQmF1nhLsjb61WWZ58wTwMIoJ1lDXEnInBc0usPxg82wSZxsHluoJ3iA%3D%3D" rel="nofollow">https://blog.csdn.net/u014125...</a><br><a href="https://link.segmentfault.com/?enc=BggORJSsvrbK1N%2F7H8u30w%3D%3D.ZHzWzdbXbTMt8gMsyynh8pEPfxSNXY78Ewt5J7QZ2EUJcYDyeEHmnUku4WhyJLQ1" rel="nofollow">https://zhuanlan.zhihu.com/p/...</a><br><a href="https://link.segmentfault.com/?enc=sUJ5YM9GSr1srxwBZVbQlQ%3D%3D.90DUcfQ0XQg5KBWk49QFHxDk0R20qd4zWyvBzuv0%2FGNs5RzlkEKJc2alXn7sXU4P" rel="nofollow">https://hustyichi.github.io/2...</a><br><a href="https://segmentfault.com/a/11900000">https://segmentfault.com/a/11...</a></blockquote>
深入理解 JS 中的类型转化
https://segmentfault.com/a/1190000038443518
2020-12-10T20:35:14+08:00
2020-12-10T20:35:14+08:00
jarbinup
https://segmentfault.com/u/jarbinup
4
<p>哪些操作能导致类型转换呢</p><h4>if 条件判断</h4><p>为 false 的值 -> false undefined null 0 '' NaN</p><h4>运算符操作</h4><p>常见的运算符 + - * /<br><code>+</code> 比较特殊除了相加之外 还有字符串拼接的含义</p><p>1)数字和非字符串相加</p><pre><code class="javascript">1 + null -> 0;
// undefined 比较特殊 表示未定义的,不是一个数字
1 + undefined -> NaN
// 会把空对象转换成数字,如果转换不成数字就变成字符串拼接
1 + {} -> 1[object object]
</code></pre><p>2)非数字相加</p><pre><code class="javascript">// 把两边都转化成数字
true + true -> 2
// 如果有一方是字符串,则进行字符串拼接
true + {} -> true[object object]</code></pre><h4>valueOf / toString</h4><p>对象的原型链上有 valueOf 和 toString 两个方法</p><pre><code class="javascript">let obj = {
symbol[toPrimitive](){
return 500
},
valueOf(){
return 100
},
toString(){
return 200
}
}
// 两边都转换成数字 obj 先调用 valueOf valueOf 如果返回不是数字 则继续调用 toString 方法
true + obj -> 101</code></pre><p><code>symbol[toPrimitive]</code> 是对象内置属性,当一个对象要转换成对应的原始类型时,会调用此方法。</p><p>总结下,当对象要进行类型转换时,会依次调用 <code>symbol[toPrimitive]</code> <code>valueOf</code> <code>toString</code> </p><h4>+ 、- 、!</h4><p><code>+</code> <code>-</code> 和 <code>!</code> 一样,可以放在变量前面,进行快速类型转换</p><pre><code class="javascript">1 + +'1234' -> 1235
1 + '1234' -> '11234'</code></pre><h4>比较元算 > = <</h4><p>1)数字和数字直接比较</p><p>2)字符串比较</p><pre><code class="javascript">// 字符串和字符串,比较的是 AscII 码
console.log('a'.charCodeAt(0))
console.log('b'.charCodeAt(0))
'a' < 'b' -> true
'a' < 'bdede' -> true // 一样的因为比的是第一位
// 数字和字符串相比,字符串先转化成数字,如果转化不成数字 这比较始终返回 false
1 < '123' -> true
1 < '1df' -> false
// 如果是对象和字符串相比,需要把对象先转化成基本类型(字符串之后再比较)
[] == '' -> true
// [].valueOf 为[],继续调用 [].toString 为 '',比较返回 true</code></pre><p>3) == 比较</p><p>如果一方是数字,会先把另一方转换成数字 然后比较</p><p>若果其中一方是 boolean 类型 会把 boolean 类型转换为数字</p><pre><code class="javascript">null == undefined // true</code></pre><p>null、undefined 和任何类型相比 <code>==</code> 都返回 false </p><p>NaN 和任何类型相比返回 false 包括它本身</p><h4>举例</h4><pre><code class="javascript">console.log([] == ![]);
// [] == false 单目运算优先级最高 为 false 的情况 false undefined NaN null 0 ''
// [] == 0 [].valueOf() -> [] 不是原始类型继续调用 toSting
// [] == 0 [].toString() -> ''
// '' == 0 Number('')
// 0 == 0 true</code></pre><p>(完)</p>
从零实现一个 promise
https://segmentfault.com/a/1190000038433512
2020-12-10T09:38:13+08:00
2020-12-10T09:38:13+08:00
jarbinup
https://segmentfault.com/u/jarbinup
23
<p>切入点从场景描述出发,即先定义好我们要实现的功能</p><h4>执行器函数</h4><p>构造函数入参 executor 自执行函数。会在在 new 的时候同步执行,传入 resolve 和 reject 状态扭转函数。自执行函数内部根据异步任务执行结果(成功或失败)调用状态扭转函数,把状态传递给后续的 then。</p><h4>状态转化</h4><p>promise 有三种状态,默认是 pending,成功之后为 fulfilled,失败之后为 failed。且状态一旦发生改变,之后不可再改变也不可逆</p><h4>then 方法</h4><p>then 方法接受两个函数,表示成功、失败状态下的回调函数。回调函数中 return 中值会当作参数传给后续的then。以此实现链式调用。</p><p>判断一个变量是否为 promise 需要两个条件,首先要是对象,然后对象上有 then 方法,以此得出 then 执行的结果也是一个 promise</p><h4>实现</h4><pre><code class="javascript">class Promise {
constructor(executor){
this.status = 'pending',
this.value = undefined;
this.reason = undefined;
this.onFulfilled =undefined;
this.onRejected = undefined;
this.onFulfilledList = [];
this.onRejectedList = [];
try {
executor(this.resolve, this.reject);
} catch(e) {
this.reject(e)
}
}
resolve(value){
if(this.status == 'pending') {
this.status = 'fullfilled';
this.value = value;
this.onFulfilledList.forEach(item => item())
}
}
reject(reason){
if(this.status == 'pending') {
this.status = 'rejected';
this.reason = reason;
this.onRejectedList.forEach(item => item())
}
}
then(onFulfilled, onRejected){
// then 之后要返回一个新的 promise
let result;
if(this.status == 'fullfilled' && onFulfilled) {
result = onFulfilled(this.value)
return Promise.resolve(result);
}
if(this.status == 'rejected' && onRejected) {
result = onRejected(this.reason);
return Promise.resolve(result);
}
if(this.status == 'pending') {
onFulfilled && this.onFulfilledList.push(()=>onFulfilled(this.value));
onRejected && this.onRejectedList.push(() => onRejected(this.reason));
}
}
}
Promise.resolve = function(value) {
if(typeof value == 'object' && value.then) {
return value;
} else {
return new Promise((resolve, reject)=>{
resolve(value)
})
}
}
Promise.all = function(list) {
return new Promise((resolve,reject) => {
let result = [];
let count = 0;
for(let i = 0; i < list.length; i++) {
if(typeof list[i] == 'object' && list[i].then) {
Promise.resolve(list[i]).then(data =>{
result[i] = data;
count++;
},reject)
}else {
result[i] = list[i];
count++;
}
}
if(count == list.length) {
resolve(result);
}
})
}
// Promise.race 同理,只要有一个成功,全部 resolve
// Promise.finally 不管成功失败,传递的回调函数都会执行,执行之后仍然可以跟then</code></pre><p>(完)</p>
react hooks源码深入浅出(一)
https://segmentfault.com/a/1190000038431635
2020-12-09T21:59:53+08:00
2020-12-09T21:59:53+08:00
了不起的小六先生
https://segmentfault.com/u/liaobuqidexiaoliuxiansheng
15
<blockquote>react hooks在react 16.8版本推出后就广受好评,因为它很好的解决了旧版本react无法优雅的复用状态逻辑的问题,同时官方说明hooks会向后兼容不存在breaking changes,在项目中更好的无缝接入。</blockquote><h3>背景和意义</h3><ol><li>目前项目中hooks使用越来越普及,我们作为开发者不仅要知其然还要知其所以然</li><li>让我们在使用过程中能更快的定位排查问题、性能调优</li><li>学习和了解优秀框架的实现思路</li></ol><h3>从两个阶段出发分析</h3><ol><li>初次渲染</li><li>更新阶段</li></ol><h3>DEMO</h3><p>我们以最基本的demo开始,其中涉及两个基本的hook:</p><ul><li>useState</li><li>useEffect</li></ul><pre><code class="javascript">import React ,{useState,useEffect} from 'react';
const Example = props => {
const [count,setCount] = useState(0);
useEffect(()=> {
console.log('xiaoliu test');
},[count])
return (
<div>
<p>You click {count} me</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
export default Example;</code></pre><h3>一、初次渲染</h3><h4>核心流程</h4><p><img src="/img/remote/1460000038431642" alt="在这里插入图片描述" title="在这里插入图片描述"><br>上图就是我们某一个hook组件在初次渲染时所经历的核心流程,大致分为三步:</p><ol><li><strong>组件类型解析(是Function、class类型),然后去执行对应类型的处理方法</strong></li><li><strong>判断当前为Function类型组件,所以执行相应方法。首先在当前组件(<code>也就是当前fiberNode,react16引入了fiber概念,每一个组件对于react来说其实就是一个fiber节点,所有的fiber节点构成了一颗fiber树</code>,具体概念参考:<a href="https://segmentfault.com/a/1190000018250127">https://segmentfault.com/a/11...</a>)上进行hook的创建和挂载,将我们所有的hook api挂载到全局变量上(全局变量为dispatcher)</strong></li><li><strong>然后顺序执行当前组件,每遇到一个hook api通过next把它连接到我们的当前fiberNode的hook链表上</strong></li></ol><h4>fiberNode结构</h4><p>初次渲染完成后的当前fiberNode(组件)中的结构关系可以用下图表示:<br><img src="/img/remote/1460000038431639" alt="在这里插入图片描述" title="在这里插入图片描述"></p><h4>源码一览</h4><p><strong>hook api挂载</strong></p><p>初次渲染<code>currentDispatcher</code>为空,先挂载所有hook到当前fiberNode的dispatcher,其实也就是将<code>HooksDispatcherOnMountInDEV</code>变量赋值到dispatcher上</p><pre><code class="javascript"> {
// 首次执行currentDispatcher = null,所以进入else分支;在更新阶段会进入if分支
if (currentDispatcher !== null) {
currentDispatcher = HooksDispatcherOnUpdateInDEV;
} else {
currentDispatcher = HooksDispatcherOnMountInDEV;
}
}</code></pre><p><strong>看看HooksDispatcherOnMountInDEV内部做了什么</strong><br>发现正是我们熟悉的useState等各种原生hook,他们内部其实是调用的mountXXX方法</p><pre><code class="javascript">HooksDispatcherOnMountInDEV = {
useCallback: function (callback, deps) {
return mountCallback(callback, deps);
},
useEffect: function (create, deps) {
return mountEffect(create, deps);
},
useMemo: function (create, deps) {
return mountMemo(create, deps);
},
useState: function (initialState) {
return mountState(initialState);
}
}
</code></pre><p><strong>回到我们的demo,首先是mountState</strong><br> 看看内部做了什么<br><img src="/img/remote/1460000038431638" alt="在这里插入图片描述" title="在这里插入图片描述">其实做了三件事:</p><ol><li><strong>创建当前hook节点,节点的数据结构为上图红框。memorizedState是我们最终返回的初始值;queue其实是更新队列,当我们多次更新某一状态时需要用queue队列存取和遍历;next用来连接下一个hook</strong></li><li><strong>将当前hook连接到当前的fiberNode的hook链表上</strong></li><li><strong>绑定状态更新方法(dispatchAction),并返回[state,dispatchAction]</strong></li></ol><p><strong>继续看demo,到useEffect,内部实际上执行mountEffectImpl方法</strong></p><pre><code class="javascript">function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 创建并获取当前hook节点信息
var hook = mountWorkInProgressHook();
hook.memoizedState = pushEffect(HasEffect | hookEffectTag, create, undefined, nextDeps);
}</code></pre><pre><code class="javascript">function mountWorkInProgressHook() {
// 将当前hook连接到我们的hook链表中
var hook = {
memoizedState: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}</code></pre><pre><code class="javascript">function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag, // 更新标识
create: create, // 传入的回调,也就是我们开发时的第一个参数
destroy: destroy, // return 的函数,组件销毁时执行的函数
deps: deps, // 依赖项数组
next: null
};
var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;
// 这里做的就是把每个useEffect hook单独以链式结构存到了componentUpdateQueue这个全局变量中
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
return effect;
}</code></pre><p>综上useEffect内部做了两件事:</p><ol><li><strong>先执行<code>mountWorkInProgressHook</code>方法,就是将当前hook连接到我们的fiberNode的hook链表中</strong></li><li><strong>定义effect对象存储我们传入的信息,同时将hook存入<code>componentUpdateQueue</code>更新队列(这个队列是用来专门存储useEffect hook的,因为useEffect的回调是异步执行的,所以在一次调和结束页面渲染后会去遍历<code>componentUpdateQueue</code>更新队列遍历执行我们存入的effect回调)</strong></li></ol><p>至此我们初次渲染结束,我们此时fiberNode的hook链式结构为</p><pre><code class="javascript">// 当前fiber节点的内部hook链
currentFiber:{
...
memoizedState:{
memoizedState:xxx,
...
next:{
memoizedState:xxx,
...
next:{
memoizedState:xxx,
...
next:hook4
}
}
}
}
</code></pre><p>更直观一些看的话如下图所示,其实就是一个单向链表<br><img src="/img/remote/1460000038431641" alt="在这里插入图片描述" title="在这里插入图片描述"></p><p>初次渲染阶段我们根据两个基础hook深入源码来分析了一下它内部的处理机制,那么在状态更新阶段它内部又是如何实现的?(未完待续...)</p>
一道打印顺序引发的血案
https://segmentfault.com/a/1190000038430623
2020-12-09T19:38:36+08:00
2020-12-09T19:38:36+08:00
jarbinup
https://segmentfault.com/u/jarbinup
1
<h2>一道打印顺序引发的血案</h2><p>异步编程是 JS 中重要的模块,前几日逛 B 站,刷到这么一个提问,拿来研究一下:</p><pre><code class="javascript">let promise1 = Promise.resolve()
.then(res => console.log(1))
.then(res => console.log(2))
let promise2 = new Promise(resolve => {
setTimeout(() => {
console.log(6)
resolve()
})
}).then(res => console.log(3))
async function main() {
console.log(4)
console.log(await Promise.all([promise2, promise1]))
console.log(5)
return { obj: 5 }
}
let promise3 = Promise.resolve()
.then(res => console.log(8))
.then(res => console.log(9))
console.log(typeof main())</code></pre><h5>变量对象声明</h5><p>首先会创建变量对象,声明函数和变量,函数的声明优先级比变量高。会变成</p><pre><code class="javascript">VO = {
main: reference async function(){},
promise1: undefined,
promise2: undefined,
promise3: undefined,
}</code></pre><p>接下来 代码执行阶段,首先会对变量进行赋值:</p><p>对 promise1 来说:<br><code>Promise.resolve()</code> 会立即执行<br><code>then(res => console.log(1)).then...</code> 会放入本轮的微任务</p><p>对 promise2 来说<br>开启一个 setTimeout 宏任务,之后的 then 会在宏任务结束后的 当次 微任务 中执行</p><p>对 promise3 来说:<br><code>Promise.resolve()</code> 会立即执行<br><code>then(res => console.log(8)).then...</code> 会放入本轮的微任务</p><p>进入 main 函数:<br>执行 <code>console.log(4)</code> <br>执行 <code>console.log(await Promise.all([promise2, promise1]))</code> <br>这一步是整个函数最关键的地方。</p><h5>async...await 执行顺序</h5><p>async...await 是 generator 的语法糖。本质是 generator + co 库,内置函数执行器。会自动执行每一个 yeild,把最后执行的结果通过 Promise resolve 的方式抛出。方便链式调用。<br>我们把代码通过 babel 转译,可以看下面代码中注释的 1 、2 两个地方。</p><pre><code class="javascript">"use strict";
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error); return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function () {
var self = this,
args = arguments;
// 2. 执行 async 首先返回一个 promise
// 返回之前会先执行完 Promise 里面的 executor 函数
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); }
_next(undefined);
});
};
}
var promise1 = Promise.resolve().then(function (res) {
return console.log(1);
}).then(function (res) {
return console.log(2);
});
var promise2 = new Promise(function (resolve) {
setTimeout(function () {
console.log(6);
resolve();
});
}).then(function (res) {
return console.log(3);
});
function main() {
return _main.apply(this, arguments);
}
function _main() {
// 1. main 函数开始真正执行的是 _asyncToGenerator
_main = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
// 3. regeneratorRuntime 函数来自 facebook 的 regenerator 模块,用来编译 generator 函数
// mark 函数为 目标函数的原型链上注入了 next、throw、return 等函数
// wrap 函数包装一层,把next 的调用指向外部的 context
// 这一步核心可以看 regenerator 源码
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
console.log(4);
_context.t0 = console;
_context.next = 4;
return Promise.all([promise2, promise1]);
case 4:
_context.t1 = _context.sent;
_context.t0.log.call(_context.t0, _context.t1);
console.log(5);
return _context.abrupt("return", {
obj: 5
});
case 8:
case "end":
return _context.stop();
}
}
}, _callee);
}));
return _main.apply(this, arguments);
}
var promise3 = Promise.resolve().then(function (res) {
return console.log(8);
}).then(function (res) {
return console.log(9);
});
console.log(typeof(main()));</code></pre><p>等待 <code>console.log(await Promise.all([promise2, promise1]))</code> 执行完,这一行比较有迷惑性,promise1 和 promise2 的执行前面已经触发,此处只是等他们的异步全部执行完。<br>之后的执行就是同步操作了</p><p>自此完整输出顺序为:</p><pre><code class="javascript"> 4
'object' //async 函数返回
1
8
2
9
6
3
[undefined, undefined]
5
</code></pre><h5>regenerator</h5><p>关于 regenerator 模块的参考资料,感兴趣的可以了解以下资料</p><blockquote>1、<a href="https://link.segmentfault.com/?enc=eYwMqBBSXzI9Gg4%2BmJ2umw%3D%3D.xENKJ4FkWP4xBB76cQTn0pKR4hPA99ecJVLGwXsego9HuBiNuLxsmixqVv4IbdBF" rel="nofollow">https://www.iteye.com/blog/schifred-2369320</a><br>2、<a href="https://link.segmentfault.com/?enc=DTjCGrznNbqrma3FXc4w1g%3D%3D.5FIcMBEaeUzU%2BLOAB%2FDrLC1UJ8dgoc34r2PAg9RKg6DSy3VPGHweI2CcC7sqQGYTtjOOgkJIUS0Zi%2BiPpNLvkA%3D%3D" rel="nofollow">https://juejin.cn/post/6844903701908291592</a></blockquote>
有赞美业前端: 持续标准化 Code Review
https://segmentfault.com/a/1190000025141916
2020-09-27T16:13:28+08:00
2020-09-27T16:13:28+08:00
边城到此莫若
https://segmentfault.com/u/bianchengdaocimoruo
55
<blockquote>关键字:代码质量 团队建设 流程优化</blockquote><h2>一、背景</h2><h4>1. 技术栈</h4><p>美业技术团队前端对外的业务项目的主要编程技术栈是:</p><p><img src="/img/bVbRENx" alt="image" title="image"></p><h4>2. 团队架构</h4><p>在构建项目的前期,前端对业务项目按端来划分人员,各端各司其职,各自沉淀。</p><p>中期随着产品的基本成型,前端团队人员按照业务领域划分成了多个子业务组前端,各组负责4端中对应模块的业务。</p><p>于是,我们美业团队20个前端几乎每个人都要维护4个不同的编码上下文的项目,好处是技术多样性丰富,但是瓶颈也同样存在,一个人需要拥有多端的开发能力,在编码规范和代码风格检查尽可能统一的情况下,因为上述技术体系的差异,我们还是不得不需要熟悉四端的技术架构、开发流程、数据流处理、资产市场、最佳实践。</p><p>这是很有挑战的,业务小组之间成立了一个小型的前端技术委员会,来应对这种变化:</p><ul><li>总结原先的项目技术规范,统一宣讲、培训、文档化</li><li>打造统一标准化的研发流程</li><li>并且持续汲取新的实践并迭代</li></ul><p><img src="/img/bVbREKU" alt="image" title="image"></p><h4>3. 代码质量问题</h4><p>随即,我们在代码质量上迎来了一些问题:</p><ul><li>项目Bug较多,同样的坑不同的人会踩</li><li>迭代后的代码难维护,包括代码可读性差、复用度低等</li><li>模块的整体设计也欠缺,扩展能力难以支撑业务发展。</li></ul><p>对代码质量的把控方面,现状流程是:我们半年要对几端的项目代码进行一次整体的code review。</p><p>但是和垃圾回收一样,整体的标记清除占用人员的时间较长,会影响届时涉及人员的业务开发进度。</p><p>于是我们想探索一种适合我们团队和业务发展,小步快跑模式的code Review,尽可能早的从一开始就参与进来,更高频率,增强审查和设计把控,减少后面返工和带来Bug所影响的整体效率。</p><h2>二、定义需求</h2><p>有了这样的背景和改进诉求,我发现我们得重新定义一下我们做这件事情的目的和价值。</p><p>经过思考和讨论,我们做 Code Review 的核心目的只有两个:</p><h4>1. 从源头把控代码质量和效率</h4><ul><li>统一代码评判标准和认知</li><li>发现边界问题</li><li>提出改进建议</li></ul><h4>2. 共享和迭代集体代码智慧</h4><ul><li>交流计思路和编码实践</li><li>沉淀最佳实践</li><li>迭代统一规范</li></ul><p>同时要做上述理想中的 Code Review,我们可能不得不面临这些实践过程中会遇到的问题:</p><p><img src="/img/bVbRFdW" alt="image" title="image"></p><p>基于这些诉求和待解决问题,我们需要对整体的标准和每一次 Code Review 的关键控制点进行细化和量化,于是有了我们第一版 Code Review 的 SOP(标准作业流程)。</p><h2>三、V1.0 标准化</h2><h4>1. 建立规范</h4><h4>1.1 宣讲</h4><p>宣讲各端统一代码规范和最佳实践、编码原则。Code Review的基础是有基本的代码规范和原则,同时要同步给大家。</p><h4>1.2 review 小组</h4><p>成立专门的 code review 小组,小组成员是各端经验丰富的人员,这样才能比较好的保障初期 Review 有比较好的效果,可以让 Code 人员有更大所获,先富带后富,经过多次 Code Review 对齐标准之后,更多 Code 优秀的同学也可以加入进来,讨论对规范和原则的实践。</p><h4>1.3 设定可迭代的代码质量评价维度和标准:</h4><p>每项1~5分,基准分为3分,得分在此基础上根据评分点浮动,总评分为各项得分的平均分。</p><p>① 基本面:对团队既定规范的遵循和代码开发的改动量是我们做评分的第一个基础。</p><ul><li>难度大、工作量大,可酌情加分</li><li>是否符合基本规范</li></ul><p>② 架构设计:是否有整体设计,设计是否合理,设计是否遵循了设计,这是第二个维度</p><ul><li>如果有设计文档,是否按照设计文档思路来写代码,是可酌情加分</li><li>review的人是否发现了更好的解决方案</li><li>代码是否提供了很好的解决思路</li></ul><p>③ 代码:代码细节上是否尽可能保持简单和易读,同时鼓励细节优化是我们的第三个维度</p><ul><li>是否明显重复代码</li><li>是否合理抽取枚举值,禁止使用“魔法值”</li><li>是否合理使用已有的组件和方法</li><li>对已有的、不合理的代码进行重构和优化</li><li>职责(组件、方法)、概念是否清晰</li></ul><p>④ 健壮性:错误处理、业务逻辑的边界和基本的安全性是我们的第四个维度</p><ul><li>边界和异常是否考虑完备</li><li>在review阶段是否发现明显bug</li><li>是否考虑安全性(xss)</li></ul><p>⑤ 效率:是否贡献了整体,为物料库和工具库添砖加瓦,与整体沉淀形成闭环,是我们第五个维度的初衷</p><ul><li>是否抽取共用常量到beauty-const库,加分</li><li>是否抽取沉淀基础组件和通用业务组件到组件库,加分</li></ul><h4>1.4 申请格式</h4><p>Code 人在企业微信群发起 Review 申请,统一参考格式,内容包括:</p><blockquote>mr地址、产品文档、UI稿、技术设计、效率平台、接口文档</blockquote><p>原则是能为 Review 方尽可能提供足够的信息来判断 Code 的好坏,去更好发现深层次问题。<br>当然文档也说不到全部的上下文,所以我们需要分配 Review 人员时候要考虑技术栈和业务相关熟悉度,必要时候 Code 人员要向 Review 人员口述需求、代码思路和重点。</p><h4>1.5 review 要求</h4><ul><li>code review 必须在提测前进行,确保上线前能够完成 Review。</li><li>review 人根据 code review 评分标准 打出各项评分,计算出本次 code review 总评分</li><li>有需要可在备注中说明原因,代码相关的备注可以直接在 gitlab 进行</li><li>code 需要解决反馈的问题</li><li>要求提测邮件中体现 code review 情况(评分、遗留问题)</li><li>mr 统一由 feature 分支合到 release 分支</li><li>review 记录,定期同步分享</li></ul><h4>1.6 review 重点</h4><ul><li>建议check代码改动范围,重点关注核心代码改动的影响</li><li>review可以针对重点代码进行</li><li>每项checklist,如果有不符合checklist内容的,请在后面【评分解释】中具体指出</li><li>「讨论沉淀」内容可包括但不限于:技术设计情况、review过程中发现的亮点与不足、值得探讨的东西、发现的bug</li><li>周会定期同步 review 情况,分享优秀代码</li></ul><h4>2. 单次流程</h4><p><img src="/img/bVbREOV" alt="image" title="image"></p><h4>3. 产出示例</h4><p><img src="/img/bVbREO2" alt="image" title="image"></p><ul><li>通过统一提交文档和口述,Code 方养成了技术设计和理清重点和评估影响范围的习惯。</li><li>通过讨论和反馈 Code Review 双方都从讨论中获取到了编码智慧的碰撞和交流,整体的代码水平总和得到了提升。</li><li>通过 Review,代码的总体设计和细节得到了更好的保障。</li><li>通过沉淀最佳实践和改进建议,团队规范和 Code Review 形成了内循环。</li></ul><h2>四、V2.0 平台化</h2><p>1.0版本在持续的细节迭代,做到了比较满意的标准化作业,但是有几个比较大的缺陷:</p><p>1.操作欠缺自动化</p><ul><li>流程的很多环节明显可以自动化,节省重复的工作量</li><li>对流程的把控依赖人,容易执行不到位</li></ul><p>2.信息欠缺数字化</p><ul><li>对 code review 的评分统计需要人工,工作量大</li><li>code review 的总览和数据分析可以支撑更好的判断团队问题和决策提升整体代码质量的策略</li></ul><p>3.流程欠缺可视化</p><ul><li>所有流程应该是可以大盘总览,单个详情全面的</li><li>每个code review事务的状态是可见的</li></ul><p>所以我们有了把 Code Review 整套流程做成已有的内部前端工具平台中一个模块的想法,以期达到可视化、自动化、数字化的目的。</p><p>投入产出比是我们需要考虑的,我们很笃定。因为虽然这件事情没有直接的业务价值,但是有非常好的质量把控和能力量化的价值,并且有标杆式的团队建设价值,人员成长了,更好地为业务服务。</p><h4>1. 需求分析</h4><p><img src="/img/bVbREPa" alt="image" title="image"></p><p>在完成上述基本需求之后,我们同时在收集反馈新增了</p><ol><li>code 人可指定 review 人员</li><li>支持项目多端配置</li><li>首页 review 得分榜排名展示</li><li>最佳实践统一导出</li><li>打通关联项目平台串联项目</li></ol><h4>2. 技术设计</h4><p><img src="/img/bVbREPg" alt="image" title="image"><br><img src="/img/bVbREPk" alt="image" title="image"></p><p>结合数据库表设计之后,我们就分工开整了。</p><h4>3. 产品效果图</h4><p><img src="/img/bVbREPl" alt="image" title="image"><br><img src="/img/bVbREPH" alt="image" title="image"><br><img src="/img/bVbREPV" alt="image" title="image"></p><ul><li>Code Review 平台化之后,打通了相关平台,自动化了人工的重复操作,聚焦在 Code 和 思考上面,无需关心流程状态。</li><li>团队对 Code 情况有了更好的全局性把控,能够进一步根据情况和数据分析对代码质量提升做决策。</li></ul><h2>五、可持续保障机制</h2><p>前人种树,后人除了乘凉之外得继续浇灌。流程和机制是死的,我们得用一些更加有温度的一些策略让它持续可以迭代和发展继承下去。</p><ol><li>半年度颁奖:半年我们会把半年大家的评分成绩统计出来,做一次激励,树立标杆,鼓励大家继续写出更好的代码,也继续的配合 Code Review。</li><li>专人 Owner:作为一个技术项目来持续维护和收集反馈意见迭代,服务小伙伴,为团队建设助力。</li><li>纳入考核:作为复杂的大型 SaaS 项目的开发者,代码能力是我们考核小伙伴专业能力的重要维度。</li></ol><blockquote>附带一些半年颁奖的图:</blockquote><p><img src="/img/bVbREN1" alt="image" title="image"><br><img src="/img/bVbREN9" alt="image" title="image"><br><img src="/img/bVbREN7" alt="image" title="image"><br><img src="/img/bVbREOg" alt="image" title="image"><br><img src="/img/bVbREOy" alt="image" title="image"></p><p>本文篇幅有限,实践过程中很多的细节问题和处理没有阐述,比如 code、review 双方的协作处理等。欢迎进一步讨论。</p><blockquote>微信:zz94530</blockquote><hr><p>目前有赞深圳研发团队大量招聘高级前端,欢迎咨询和投简历~</p>
走进AST
https://segmentfault.com/a/1190000023375905
2020-07-25T22:41:01+08:00
2020-07-25T22:41:01+08:00
csywweb
https://segmentfault.com/u/csywweb
16
<blockquote>前言:AST已经深入的存在我们项目脚手架中,但是我们缺不了解他,本文带领大家一起体验AST,感受一下解决问题另一种方法</blockquote>
<h2>什么是AST</h2>
<p>在讲之前先简单介绍一下什么AST,抽象语法树(<code>Abstract Syntax Tree</code>)简称 <code>AST</code>,是源代码的抽象语法结构的树状表现形式。<br>平时很多库都有他的影子:<br><img src="/img/bVbKcIi" alt="image.png" title="image.png"><br>例如 <code>babel</code>, <code>es-lint</code>, <code>node-sass</code>, <code>webpack</code> 等等。</p>
<p>OK 让我们看下代码转换成 <code>AST</code> 是什么样子。</p>
<pre><code>const ast = 'tree'</code></pre>
<p>这是一行简单的声明代码,我们看下他转换成AST的样子</p>
<p><img src="/img/bVbKcPK" alt="image.png" title="image.png"></p>
<p>我们发现整个树的根节点是 <strong>Program</strong>,他有一个子节点 <strong>body</strong>,<strong>body</strong> 是一个数组,数组中还有一个子节点 <strong>VariableDeclaration</strong>,<strong>VariableDeclaration</strong>中表示<code>const ast = 'tree'</code>这行代码的声明,具体的解析如下:</p>
<pre><code>type: 描述语句的类型,此处是一个变量声明类型
kind: 描述声明类型,类似的值有'var' 'let'
declarations: 声明内容的数组,其中每一项都是一个对象
------------type: 描述语句的类型,此处是一个变量声明类型
------------id: 被声明字段的描述
----------------type: 描述语句的类型,这里是一个标识符
----------------name: 变量的名字
------------init: 变量初始化值的描述
----------------type: 描述语句的类型,这里是一个标识符
----------------name: 变量的值</code></pre>
<p>大体上的结构是这样,body下的每个节点还有一些字段没有给大家说明,例如:位置信息,以及一些没有值的key都做了隐藏,推荐大家可以去 <a href="https://link.segmentfault.com/?enc=Ul3cG7GwuuSZhpvio0ojAA%3D%3D.s5phdsKxBgm7zdkBvOfO5J35bK%2FjUxZwuhLINbAeXm4%3D" rel="nofollow">asteplorer</a>这个网站去试试看。</p>
<p>总结一下, AST就是把代码通过编译器变成树形的表达形式。</p>
<h2>如何生成AST</h2>
<p>如何生成把纯文本的代码变成AST呢?编辑器生成语法树一般分为三个步骤</p>
<ul>
<li>词法分析</li>
<li>语法分析</li>
<li>生成语法树</li>
</ul>
<ol><li>词法分析:也叫做<strong>扫描</strong>。它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识tokens。同时,它会移除空白符,注释,等。最后,整个代码将被分割进一个tokens列表(或者说一维数组)。</li></ol>
<p>比方说上面的例子 <code>const ast = 'tree'</code>,会被分析为<code>const、ast、=、'tree'</code></p>
<pre><code>const ast = 'tree';
[
{ type: 'keyword', value: 'const' },
{ type: 'identifier', value: 'a' },
{ type: 'punctuator', value: '=' },
{ type: 'numeric', value: '2' },
]
</code></pre>
<p>当词法分析源代码的时候,它会一个一个字母地读取代码,所以很形象地称之为扫描-scans;当它遇到空格,操作符,或者特殊符号的时候,它会认为一个话已经完成了。</p>
<p>2.语法分析:也称为解析器。它会将词法分析出来的数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。</p>
<p>3.生成树:当生成树的时候,解析器会删除一些没必要的标识tokens(比如不完整的括号),因此AST不是100%与源码匹配的,但是已经能让我们知道如何处理了。说个题外话,解析器100%覆盖所有代码结构生成树叫做CST(具体语法树)</p>
<h4>能否通过第三方库来生成?</h4>
<p>有很多的第三方库可以用来实战操作,可以去<a href="https://link.segmentfault.com/?enc=WalMF0swA16OaDbu1j0EcA%3D%3D.s7%2FmfcqdSGwDJ400To3%2BsCST44mu2yCg25f5ILII7y8%3D" rel="nofollow">asteplorer</a>这个网站去找你喜欢的第三方库,这里不限于<code>javascript</code>,其他的语言也可以在这个网站上找到。<br>如图:<br><img src="/img/bVbKfc1" alt="image.png" title="image.png"></p>
<p>关于<code>javascript</code> 的第三方库,这里给大家推荐 <code>babel</code> 的核心库<code>babylon</code></p>
<pre><code class="javascript">// yarn add babylon
import * as babylon from 'babylon';
const code = `
const ast = 'tree'
`
const ast = babylon.parse(code); // ast
</code></pre>
<h2>如何实践</h2>
<p>ok,现在我们已经知道如何把我们的代码变成 <code>AST</code> 了,但是现实中,我们经常会使用到代码的转换,比方说 jsx -> js, es6 -> es5, 是的就是 <code>babel</code>,我们来看看<code>babel</code>是如何转换代码的。</p>
<p>大体上<code>babel</code>转换代码分为三步</p>
<pre><code>1. 通过`babylon`生成`AST`
2. 遍历`AST`同时通过指定的访问器访问需要修改的节点
3. 生成代码</code></pre>
<p>看一个简单的例子一起理解一下<br>生成<code>AST</code></p>
<pre><code class="javascript">import * as babylon from 'babylon';
// 这里也可以使用 import parser from '@babel/parser'; 这个来生成语法树
const code = `
const ast = 'tree'
console.log(ast);
`
const ast = babylon.parse(code); // ast</code></pre>
<p>遍历<code>AST</code>同时通过访问器<code>CallExpression</code>来访问<code>console.log(ast)</code>并删除它</p>
<pre><code class="javascript">import traverse from '@babel/traverse'
import t from '@babel/types';
// 2 遍历
const visitor = {
CallExpression(path) {
const { callee } = path.node;
if (
t.isMemberExpression(callee) &&
callee.object.name === 'console' &&
callee.property.name === 'log'
) {
path.remove();
}
},
}
traverse.default(ast, visitor);</code></pre>
<p>生成新代码</p>
<pre><code>import generator from '@babel/generator';
generator.default(ast);</code></pre>
<blockquote>简单的答疑:CallExpression表示这是一个调用,为什么还要做更深入的判断呢,因为直接的函数调用 foo() 这也是一个CallExpression,A.foo()这也是一个CallExpression, 所以要更深入的判断</blockquote>
<p>好的,代码转换完成!值得庆祝。我们可以看到<strong>第一步生成<code>AST</code></strong>和<strong>第三步生成新代码</strong>都由<code>babel</code>替我们做了,我们真正操作的地方在于第二步:通过访问器操作需要操作的节点。</p>
<p>由此可见我们开发babel-plugin的时候,也只需要关注<strong>visitor</strong>这部分就好。</p>
<p>上述代码改为babel-plugin示例:</p>
<pre><code class="javascript">module.export = function plugin({ types: t}) {
return {
visitor: {
CallExpression(path) {
const { node } = path;
if (t.isMemberExpression(node.callee) &&
node.callee.object.name === 'console' &&
node.callee.property.name === 'log'
) {
path.remove();
}
},
},
};
}</code></pre>
<p>将这个插件加入到你的<code>babel</code>插件列表中,可以看到它真的生效了,一切都是这么简单。so amazing!</p>
<h2>结语</h2>
<p>开头提到的常用库<code>prettire</code>, <code>eslint</code>, <code>css-loader</code> 等等其实都是先生成<code>AST</code>,然后再操作<code>AST</code>,最后在生成代码。只不过操作<code>AST</code>的过程很复杂,举一反三在项目里,组件库升级,组件批量替换都可以使用这个思路。甚至可以根据业务做一些自己业务方的<code>babel-plugin</code>都行。<br>感谢您的阅读,有问题可以在评论区交流~</p>
<blockquote>帮助链接<br><a href="https://link.segmentfault.com/?enc=%2B%2FBsnDGuKQFH5jD%2FdIA4cg%3D%3D.0jX8lhVHss3KjmUFrEje8adKgvJrUO%2FnpiBsInzTBU%2FRtSBWrJ0SrbIhs%2F11UBfMKo5SX14jSfASeAE1WV5Q216pGMSBofAOi0L8YbWe%2FF5fR%2BecS2IFLOpgqfH1B9quz6wAMXVLFuZpjtifMXHA5GoJJ8oR3cXMlFfoUyGDwco%3D" rel="nofollow">如何开发一个babel-plugin</a><br><a href="https://link.segmentfault.com/?enc=vUsb6%2F6500V22Li28dhKDw%3D%3D.nHD0D6OsrAQbUrO5dA1nYu0YxWuQo8LBIMhnLlsyyEnv9rtwoXPY%2BnsFe%2BrIlPQda9uty4gZ1uvMhmwkQLK1bw%3D%3D" rel="nofollow">《AST for JavaScript developers》</a>
</blockquote>
关于DNS解析
https://segmentfault.com/a/1190000023025244
2020-06-26T17:40:07+08:00
2020-06-26T17:40:07+08:00
丶Vin
https://segmentfault.com/u/_vin_5d884143cd4cb
9
<h2>这篇文章在说什么</h2>
<p>1、域名的结构<br>2、DNS解析流程</p>
<h2>前奏</h2>
<p>在进入正题之前可以先适当的引入IP概念,以便下面的流畅阅读。</p>
<pre><code>1、[什么是IP地址?](https://segmentfault.com/a/1190000022864573)
IP地址相当于网络中的身份唯一认证ID</code></pre>
<h2>正题</h2>
<pre><code> DNS:(Domain Name System)域名解析系统</code></pre>
<blockquote>域名解析系统,听着还挺费解的,我们知道当我们浏览器输入网址的时候,输入的是一串域名,例如:<code>www.google.com</code>,但是我们在委托我们的操作系统发送消息时,却不是靠域名来找到对应的服务器,靠的的IP地址(这是TCP/IP协议的要求)。这个时候,所需要做的就是通过域名解析,来拿到我们的IP地址。</blockquote>
<h4>
<a href="https://link.segmentfault.com/?enc=hFh%2B4d1Fq4MVr5JOSQMxjg%3D%3D.HmqDQZxdbLG6R1m1DPFzcENgF3D3aC6BCcsK4W%2BKqAwIV%2F3C0DnjJehn7njCrnYWUr2pd0RF5NLaNAo0bzTWwV7UEOi3IAfxkTKf5TiZH2L2N3aW0Jj%2F%2BkB73nSNjzTK" rel="nofollow">域名</a>的结构</h4>
<blockquote>域名可以通过<code>.</code>拆分成几个部分,从右到左依次是:顶级域名、二级域名、三级域名...</blockquote>
<p><img src="/img/bVbICUU" alt="域名.png" title="域名.png"></p>
<h3>DNS解析流程</h3>
<p>所以当我们输入网址,去请求资源的话,那它又是如何办到的呢?</p>
<pre><code>简单来说:DNS解析过程属于应用层协议(不知道应用层也不影响解析流程),当我们生成http报文之后,就会在查找浏览器/host/本地/网关/本地DNS服务器/IPS/根域名服务器等中是否有DNS缓存,如果有的话,优先取缓存数据,否则,会通过主机上运行的DNS客户端(我们的计算机上会有相应的DNS客户端,又称DNS解析器)向DNS服务器发送查询报文,DNS服务器再根据查询消息返回响应内容。</code></pre>
<p><img src="/img/bVbILXn" alt="image.png" title="image.png"></p>
<h6>查询报文</h6>
<blockquote>域名、类型(表示域名对应什么类型的记录,类型为MX时,表示域名对应的是邮件服务器,类型为A时,对应的IP地址)、以及Class(Class的值用来识别网络信息,现在互联网没有其他网络,所以永远是IN)。DNS服务器会根据查询消息来查询对应的消息记录。</blockquote>
<h6>邮件查询</h6>
<blockquote>邮件的记录类型是MX,又称为邮件交换记录。它是通过邮件地址的”@“符号后面的域名,得到对应的邮件服务器。DNS服务器会返回邮件服务器的域名和优先级。(邮件地址有可能对应多个邮件服务器,需要根据优先级来判断哪个服务器优先查询。数值越小越优先。)因为最终也需要得到邮件服务器的IP地址,所以拿到邮件的服务器域名后最终又会解析成IP地址返回客户端。</blockquote>
<p><img src="/img/bVbILXy" alt="image.png" title="image.png"></p>
<hr>
<h4>演示</h4>
<p><strong>光说没用,我们可以来演示一波,当我们查询www.google.com时:</strong><br><img src="/img/bVbILXD" alt="image.png" title="image.png"></p>
<p><strong>DNS客户端的请求报文</strong></p>
<pre><code>;; QUESTION SECTION:
;www.google.com. IN A</code></pre>
<p><strong>DNS服务器返回的查询结果</strong><br>只有1个A记录代表,只有一个IP地址。221是缓存时间,代表221s内不用重新查询。</p>
<pre><code>;; ANSWER SECTION:
www.google.com. 221 IN A 8.7.198.45</code></pre>
<p><strong>NS记录</strong><br>即域名服务器记录(Name Server),用来指定该域名由那个DNS域名服务器解析。</p>
<pre><code>;; AUTHORITY SECTION:
google.com. 51 IN NS ns4.google.com.
google.com. 51 IN NS ns3.google.com.
google.com. 51 IN NS ns1.google.com.
google.com. 51 IN NS ns2.google.com.</code></pre>
<p><strong>DNS域名服务器的IP地址</strong></p>
<pre><code>;; ADDITIONAL SECTION:
ns1.google.com. 266 IN A 216.239.32.10
ns1.google.com. 197 IN AAAA 2001:4860:4802:32::a
ns2.google.com. 280 IN A 216.239.34.10
ns2.google.com. 197 IN AAAA 2001:4860:4802:34::a
ns3.google.com. 55 IN A 216.239.36.10
ns3.google.com. 104 IN AAAA 2001:4860:4802:36::a
ns4.google.com. 299 IN A 216.239.38.10
ns4.google.com. 92 IN AAAA 2001:4860:4802:38::a</code></pre>
<p><strong>邮件的DNS查询</strong><br><em>可以看到DNS服务器返回了五个服务器域名以及优先级。</em><br><img src="/img/bVbILXJ" alt="image.png" title="image.png"></p>
<h5>记录类型还有很多种</h5>
<p>想了解的可以 => <a href="https://link.segmentfault.com/?enc=%2Fr30dIQhcTM5wXfA%2FmQkzw%3D%3D.Dys1JbLgQz2M%2FMQhXK43Z3298pp4akpdl%2BL3Gi1tKzL66q7qeGw94s5l0%2FD1SUbaF6O%2FFldCbk8HVOfVfcZ8ndcfhyJyhlK7LWcUmFWpqp3bR5z7lA2jZHLXLZuAS7VU" rel="nofollow">记录类型</a><br><img src="/img/bVbILYG" alt="来自维基百科部分截图" title="来自维基百科部分截图"></p>
<hr>
<h4>当浏览器发起请求</h4>
<p>直接上图吧~说太多都没有用~<br><img src="/img/bVbICW0" alt="DNS域名解析流程.png" title="DNS域名解析流程.png"></p>
<ul>
<li>当我们的浏览器发起http请求时,首先会先查询浏览器是否有DNS缓存</li>
<li>浏览器没有缓存,则会找到计算机的本地hosts文件,是否存在映射关系。</li>
</ul>
<pre><code>hosts文件地址
Mac:/etc/hosts
Windows 7: C:\\**Windows**\\System32\\drivers\\etc
我们可以看到,下图中有域名对应着IP
就相当于告诉计算机,如果我访问这个域名,那你就去这个ip地址找资源吧~</code></pre>
<p><img src="/img/bVbIL0A" alt="image.png" title="image.png"></p>
<ul>
<li>如果hosts文件不存在映射关系,那么会去找DNS的本地缓存。</li>
<li>本地没有缓存的话,则会通过我们本地设置的DNS服务器地址,去找本地DNS服务器要资源。一般来说本地DNS服务器都会有一份缓存,如果有的话,就直接将缓存的内容传回去,没有的话,那么它就会去找根服务器。</li>
</ul>
<blockquote>
<strong>说到这里,那我们停一下,现在是不是有两个疑问</strong><br><strong>1、究竟什么是本地服务器呢?</strong><br><strong>2、如果本地有缓存又要怎么办?</strong><br>留着最后回答~<br>我们先来解释图中本地DNS服务器与DNS服务器之间的关系,以及什么是根服务器。</blockquote>
<hr>
<h6>DNS服务器之间的联系</h6>
<blockquote>DNS服务器相互之间的联系是:管理下一级域名的服务器会将自己注册到管理上级域名的DNS服务器上。</blockquote>
<p><img src="/img/bVbIL2p" alt="image.png" title="image.png"><br>所以,当我们从根域名服务器一层层往下找,就可以找到当前域名所在的DNS服务器了。</p>
<h6>什么是根域名服务器</h6>
<blockquote>前面说了域名的结构,但是在我们的互联网中,还有一个不为人知的地方,叫做根域。它处于一级域名(顶级域名)的上方,根域没有自己的名字(不配有姓名),我们在输入域名时经常省略了它。它是一个点,是的,就是一点”.”,如果要表明根域,那么域名就会写成这样:”www.youzan.com.”没在域名的最后加一个句号。一般都不会写句号。根域名服务器管理的不是所有的域名,而是管理一级域名的服务器所在地址,比如管理着com域名服务器的地址。</blockquote>
<pre><code>很多资料上说,全世界IPv4根域名服务器只有13台。
13台根域名服务器的名字是从A-M。
1个主根服务器在美国,其余为12个辅根服务器,美国(9),英国(1),瑞典(1),日本(1)。
有人是不是想问为什么中国没有?嗯,就是没有。
(因为互联网起源于美国,一开始只有美国有互联网,大部分在美国无可厚非。)
但是中国有IPv4镜像根服务器。
编号相同的镜像根服务器使用同一个IP。
所以,其实上面的说法是不精准的,根域名服务器其实有很多台,但是服务器的IP地址只有13个。
题外话:IPv6根服务器中国有4个,一个主根,三个辅根。
</code></pre>
<p><strong>主根和辅根的区别</strong>:主根和辅根的数据是一致的,当有新的域名出现时,会先更新到主根服务器,再复制到辅根服务器。</p>
<p><strong>镜像服务器</strong>:相当于镜子里的你,除了不是真正的你,也具有你的特征。就像你的桌面图标生成一个快捷方式的图标一样。</p>
<hr>
<p>现在我们了解了DNS服务器之间的联系,那么我们回到流程图中:</p>
<ul>
<li>本地DNS服务器先是去根服务器找域名的ip,根域名服务器没有,给了他com域名服务器的ip。</li>
<li>但是com域名服务器也不知道www.test.com的ip,但是知道test.com在哪台域名服务器上。</li>
<li>最终,找到了www.test.com</li>
</ul>
<h3>QA环节</h3>
<p><strong>1、究竟什么是本地服务器呢?</strong><br>当我们打开网络配置的时候,会看到有一个DNS IP地址,这个IP地址则是我们指向的本地DNS服务器地址。<br>不同的操作系统设置方式不一样,DNS服务器的地址可以是提前设置好的也可以是自动分配的,MacOS的长这样:<img src="/img/bVbIL4g" alt="image.png" title="image.png"></p>
<blockquote>在我们非手动设置的情况下:如果我们的网络是直连的运营商网络,一般而言那我们的本地DNS则是ISP运营商IP地址。<br>如果我们设置了转发(使用了路由器),那我们的地址极有可能是192.168.1.1(如上图),路由器本身,我们的路由器会将请求转发到上层DNS,也就是ISP运营商DNS服务器。</blockquote>
<p><strong>2、如果本地有缓存又要怎么办?</strong><br>所以以后如果页面打不开了,可以先清除浏览器或者电脑的DNS缓存试试,看是否是因为本地的缓存导致域名解析错误。</p>
<pre><code>清除DNS缓存:
Mac(10.13.6): sudo dscacheutil -flushcache
Window: ipconfig /flushdns
谷歌浏览器:chrome://net-internals/#events.</code></pre>
<p><strong>3、为什么服务器的IP地址只有13个?</strong></p>
<pre><code>因为DNS查询用的是UDP,而不是TCP。
UDP 实现中能保证正常工作的最大包是 512 字节,所以只能13个根服务器地址。
想要了解更多,请进入[传送门](https://jaminzhang.github.io/dns/The-Reason-of-There-Is-Only-13-DNS-Root-Servers/)</code></pre>
<p><strong>4、IPv4与IPv6的区别</strong></p>
<pre><code>IPv4:由32位二进制数组成
IPv6:可由128位二进制组成
[详文可阅读](https://zhuanlan.zhihu.com/p/50747832)</code></pre>
<p><strong>5、为什么需要域名解析,而不直接是IP?</strong></p>
<pre><code>1、域名好记,给你ip,你可以记几个ip地址哇
2、不同域名可以对应同一个IP
3、服务器IP变了咋办
4、TCP/IP协议的需要</code></pre>
<blockquote>
<em>参考资料:</em><br><em>《网络是怎么连接的》</em>
</blockquote>
IP地址的构成、相同网段、网络掩码
https://segmentfault.com/a/1190000022864573
2020-06-07T20:43:19+08:00
2020-06-07T20:43:19+08:00
丶Vin
https://segmentfault.com/u/_vin_5d884143cd4cb
6
<p><strong>看完这篇文章希望可以解答的问题是:</strong></p><pre><code>1、IP地址的构成
2、什么是网络掩码?
3、如何才算是处于相同网段的通信?
</code></pre><p><strong>看懂所需要的门槛是</strong>:二进制换算</p><pre><code>计算机之间的通信,可以分为相同网段的通信和不同网段的通信。那什么是相同网段和不同网段呢?不管三七二十一,先画个图,感受一下。</code></pre><p><img src="/img/bVbH6gd" alt="download.png" title="download.png"><br>员工A和B就属于相同网段,A与C、B与C就是不同网段。在图中我们可以看到有IP地址和网关两个玩意儿,他们究竟是什么呢?为什么又能来区分相同网段和不同网段?</p><p>在回答之前,先介绍一下什么是IP地址:</p><pre><code>IP地址相当于网络中的身份唯一认证ID,跟身份证ID一样是唯一的,唯一不同的是,IP地址是可以变的,只是不管怎么变,都将会是唯一的。Mac地址的性质更加接近于身份证ID,它是设备的唯一ID。</code></pre><p><strong>IP地址 = 网络地址 + 主机地址</strong></p><p>IP地址目前普遍是IPv4版本,由32位二进制数分成4组,每组1字节Byte(8比特Bit)组成。分别用十进制表示再用圆点隔开,就是现在的172.1.1.10。</p><p><strong>什么是网络地址和主机地址?图中172.1.1.10/24的24又指的是什么?</strong><br>说到这里不得不解释一下什么是子网掩码(又称网络掩码)</p><p>24指的是子网掩码的长度,用子网掩码来表示,就是:255.255.255.0。它的作用主要是用来区分网络地址和主机地址。</p><p>上面我们说了,员工A和B就属于相同网段。而归根究底是因为他们有相同的网络号,偏偏子网掩码又是用来告诉我们他们是真的有着相同的网络号的。</p><p>255.255.255.0用二进制表示,则是:</p><pre><code>11111111.11111111.11111111.00000000
</code></pre><p>172.1.1.10用二进制表示,则是:</p><pre><code>10101100.00000001.00000001.00001010</code></pre><p>连续24个1,也就是172.1.1.10/24中24的由来。</p><p>通过按位与最终得到网段号:</p><pre><code>10101100.00000001.00000001.00000000
</code></pre><p>按位与/& : 1 & 1 => 1 、 1 & 0 => 0 、 0 & 0 => 0<br><img src="/img/bVbH6g1" alt="download \(1\).png" title="download \(1\).png"></p><p>所以172.1.1.10中剩下的10(00001010)即是主机号,172.1.1是网段号,那回到上面的员工A、B、C中:<br> 员工A(172.1.1.10/24)的网段号:172.1.1<br> 员工B(172.1.1.11/24)的网段号:172.1.1<br> 员工C(172.1.2.10/24)的网段号:172.1.2<br> 显然A、B在同一个网段里</p><p>是不是看上去很容易了,那我们学以致用,<strong>现在有一个IP地址:172.1.1.10/25,请问,这里的网络位、主机位是多少?主机数是多少?网络地址和广播地址是多少?网络掩码是多少?</strong> </p><p>解题步骤:<br>1、首先我们将IP地址转为32位二进制:</p><pre><code>10101100.00000001.00000001.00001010
</code></pre><p>2、从地址中知道子网掩码的长度是25,总长为32Bit,那我们可以写上25个连续的1,剩下的补上0,得到:</p><pre><code>11111111.11111111.11111111.10000000 (255.255.255.128)</code></pre><p>按位与操作后,可以拿到网络位:</p><pre><code>10101100.00000001.00000001.1xxxxxxx
</code></pre><p>3、那网络地址和广播地址是什么呢,我们将上面的7个x,改为0,得到的就是网络地址(网络号),全部改为1,得到的就是广播地址。所以:</p><pre><code> 网络地址:10101100.00000001.00000001.10000000
十进制:172.1.1.128
广播地址: 10101100.00000001.00000001.11111111
十进制:172.1.1.255
</code></pre><p>4、那么我们的主机位有多少呢?</p><pre><code>172.1.1.128 ~172.1.1.255 之间(抛开网络地址和广播地址)一共可以有126个主机位。
</code></pre><p>当然这样算太累了,用一个比较简便的算法,<strong>IP地址总长 32 - 子网掩码长度 25 = 主机位 7,那么根据排列组合主机位 = 2 ^ 7 - 2 = 126</strong>,减2是减去广播地址以及网络地址。</p><p>算完上面的题目,是不是感觉清晰了很多,那问题又来了?<strong>255.255.255.198这个掩码又是不是合法呢?</strong></p><p>我们上面的掩码长度,都是连续的1,可 255.255.255.198转为二进制是:11111111.11111111.11111111.11000110</p><p>不是连续的1了,很多人认为,这样的子网掩码是不合法的。这是错误的理解,IP协议中给子网掩码提供了一定得灵活性,允许子网掩码中的0和1不连续,但是这样的子网掩码给分配主机以及找到相同网段都造成了一定的难度。市面上也只有极少路由器支持在子网中这样使用。所以实际应用中大多都是采用上述方式。</p><p>当我们的企业、公司去申请一个IP地址时,实际上拿到的是网络号,通过网络的性质以及规模,由自己的企业去自行分配主机号。</p><p>当然,网络号自然是要划分三六九等的,因为网络的规模差异比较大、而我们的IP资源有限,根据网络号和主机地址来分,主要分为A、B、C三类和特殊地址D、E(可以粗略了解,传送门:<a href="https://link.segmentfault.com/?enc=a5WpI5AFllSx1XqtYEboYA%3D%3D.wBSL1lxNQ1gl%2BI%2F%2By4%2F6Kc65aOmau138M0mkZXNtQduUqn0WukBYcocd3vVMNZrO" rel="nofollow">https://blog.51cto.com/huchina/2159073</a>)</p><p>至此,文章开头的问题,应该是有所解答了。<br><a href="https://link.segmentfault.com/?enc=o%2FUZCs5H3gP8LwbEkT5%2FlQ%3D%3D.bg%2FiNcUZkFUimRmuUGcaMorc2D%2F5K0XEb3Ih%2BvjT%2Blwivx5b17QveKzZjJmZ%2F2IU" rel="nofollow">掘金同步发布</a></p>
export default 为何突然没用了?
https://segmentfault.com/a/1190000022393344
2020-04-16T15:31:27+08:00
2020-04-16T15:31:27+08:00
csywweb
https://segmentfault.com/u/csywweb
12
<h2>前言</h2>
<p>前几天团队小伙伴问我一个问题,我在ts文件中使用解构导出了一个变量,在其他地方 <code>import</code> 进来发现是 <code>undefined</code>,类似这样</p>
<pre><code class="ts">//a.ts
export const a = {
a1: 1,
a2: 2
}
export const b = {
b1: 1
}
export default {
...a,
b
}</code></pre>
<pre><code class="ts">// b.ts
import { a1 } from 'a';
console.log(a1): // undefined</code></pre>
<p>这里抛出一个疑问?</p>
<h4>明明使用了 <code>babel-plugin-add-module-exports</code> 兼容了 <code>export default</code>,但是就是取不到?</h4>
<p>接下来我们从 export defalut -> babel -> add-module-exports来逐步的了解下为什么</p>
<h2>export default 作用是什么</h2>
<p><code>export default</code>命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此<code>export default</code>命令只能使用一次。本质上,<code>export default</code>就是输出一个叫做<code>default</code>的变量或方法,然后系统允许你为它取任意名字。</p>
<p>类似这样<br>导出函数</p>
<pre><code>//a.js
export default funcion() {
//xxx
}
//b.js
import foo from 'a';</code></pre>
<p>导出对象</p>
<pre><code>//c.js
const c = { c1:1, c2:2 }
const d = { d1:1, d2:2 }
export default {
c,d
}
//d.js
import obj from 'c'
console.log(obj); // {c:{c1:1,c2:2},d:{d1:1,d2:2}}</code></pre>
<p>导出default</p>
<pre><code>//a.js
function foo(){}
export { foo as default};
// 等同于
// export default foo;
// b.js
import { default as foo } from 'a';
// 等同于
// import foo from 'a';
</code></pre>
<p>到这里看起来一切很美好,有一个新问题:在<code>d.js</code>里,我想直接拿到 <code>obj</code> 里的 <code>c</code> 属性,可以吗?</p>
<pre><code>const c = { c1:1, c2:2 }
const d = { d1:1, d2:2 }
export default {
c,d
}
//d.mjs
import {c} from 'c'
console.log(c); // 报错了
// terminal
node --experimental-modules d.js
/*
import {c} from 'c';
^
SyntaxError: The requested module 'c' does not provide an export named 'c'
*/</code></pre>
<p>其实这样写是错的,因为ES6的import并不是对象解构语法,只是看起来比较像,可以参考MDN对import的描述<a href="https://link.segmentfault.com/?enc=XIJNmeZ3bl3e5wqlS2fxAg%3D%3D.NSyX8ZuFCwWQgd%2FBRnvhxECqevxyE7tddB8gm2mW5aKX50EbUgQF1L4caQ6%2B9aXme64a%2FpeMl1pzMflbU%2BphM6vK6oK2bQvt757D6Zo0sktgb2%2FEJ6hRXHmnqObjxhRO" rel="nofollow">MDN import</a>。所以import并不能解构一个default对象。</p>
<p>既然import不支持解构一个default对象,那么我们心中又有一个疑问,为什么我们在项目中能够随意的去写 export default, 并且通过解构可以取的到呢?</p>
<h2>export default 编译结果</h2>
<p><code>export default</code> 属于 <strong>ES6</strong> 的语法,为了兼容不支持 ES6 的浏览器,所以需要 babel 编译。接下来我们看看经过babel编译之后,<code>export default</code>变成了什么。</p>
<h3>babel 5 时代</h3>
<p>在使用babel5的时候,下面代码</p>
<pre><code>//a.js
const a = {};
const b = {};
export default {a,b}
//b.js
import {a} from 'b'
console.log(a)</code></pre>
<p>会被打包为</p>
<pre><code>//a.js
...
let _default = _objectSpread({}, {a, b});
exports.default = _default;
module.exports = exports.default;
//b.js
"use strict";
var _const = require("./a");
console.log(_const.a);</code></pre>
<p>babel 把 esm 解析成了cjs,我们执行 b.js,发现可以取到值,但是在浏览器环境require语法,我们还需要webpack,因为webpack 简单来说是对babel转换后的文件做了一层 require 的包装,所以这里具体不谈webpack做了什么,只讨论babel, webpack具体做了什么可以戳这里查看</p>
<blockquote>
<a href="https://segmentfault.com/a/1190000016524677">webpack启动代码解读</a><br><a href="https://segmentfault.com/a/1190000010955254">webpack模块化原理</a>
</blockquote>
<h3>babel 6 时代</h3>
<p>项目升级babel 6 之后,发现之前写法取不到值了,上面的 a.js 和 b.js 打包后变为</p>
<pre><code>//a.js
...
let _default = _objectSpread({}, {a, b});
exports.default = _default;
// babel6 去掉了 module.exports = exports.default;
//b.js
"use strict";
var _const = require("./a");
console.log(_const.a);</code></pre>
<p>这个时候 <code>_const</code> 的值为 <code>{default: {a:{},b{}}}</code>。<br>出现这个的原因是因为 Babel 的这个Issue <a href="https://link.segmentfault.com/?enc=kghyPBxvITKCK%2BmAmrq%2Fpw%3D%3D.K4%2B8DgyaEj5OGs%2B6fJYDeDd9xSmeePNi6Fb6Yp5dDZ%2F9QVO1Rh4DMggSzXlVgiN4" rel="nofollow">Kill CommonJS default export behaviour</a>,所以 Babel 6通过不再执行<code>module.exports = exports['default']</code>模块转换来更改某些行为。Babel5 是支持export 一个对象,但是到 Babel6 的时候去掉了这个特性。这个时候我们为了兼容老代码,需要一个解决方案,这个时候 <a href="https://link.segmentfault.com/?enc=6klVJpY7si9pq9BxlT6izw%3D%3D.WnhUO0AvYoMgML3%2BLCivH9Jj%2BAA1mu2FvRhU10PL5PvmBbHvvLw4OgNJg2sirhHLoZBj7faEokxTe0MNFkjNrw%3D%3D" rel="nofollow">babel-plugin-add-module-exports</a> 入场了。</p>
<h3>babel-plugin-add-module-exports 入场</h3>
<p><code>babel-plugin-add-module-exports</code> 主要作用是补全 Babel6 去掉的 <em>module.exports = exports.default;</em> <br>问题来了,项目中配置了<code>babel-plugin-add-module-exports</code>为什么前沿中的代码会有问题呢</p>
<h2>babel-plugin-add-module-exports 失效原因</h2>
<p>答案很简单,我们发现 babel-plugin-add-module-exports 失效了,深入源码打个log发现会判断是否有 name export,如果有 name export,就不会补上 babel5 export default object的特性。</p>
<pre><code>// hasExportNamed 一直是 true
...
if (hasExportDefault && !hasExportNamed) {
path.pushContainer('body', \[types.expressionStatement(types.assignmentExpression('=', types.memberExpression(types.identifier('module'), types.identifier('exports')), types.memberExpression(types.identifier('exports'), types.stringLiteral('default'), true)))\]);
}
...</code></pre>
<p>解决方案:<br>我们只需要改动代码为</p>
<pre><code class="ts">//a.ts
const a = {
a1: 1,
a2: 2
}
const b = {
b1: 1
}
export default {
...a,
b
}</code></pre>
<pre><code class="ts">// b.ts
import { a1 } from 'a';
console.log(a1): // 1</code></pre>
<p>我们只需要去掉 export const, 只保留 export default 即可解决这个问题。</p>
<p>有好奇的同学问,为什么有了name export,就不会去补全<code>module.exports = exports.default</code>。看到下面的例子你就明白了</p>
<pre><code class="ts">//a.ts
export const a = {
a1: 1,
a2: 2
}
const b = {
b1: 1
}
export default {
b
}</code></pre>
<pre><code class="ts">// b.ts
import { a } from 'a';
console.log(a);</code></pre>
<p>打包后手动加一个 <code>module.exports = exports.default</code>,</p>
<pre><code>//a.js
...
let _default = _objectSpread({}, {b});
exports.default = _default;
module.exports = exports.default;</code></pre>
<p>结果可想而知,在b文件require进来的时候,a 找不到了</p>
<pre><code>//b.js
"use strict";
var _const = require("./a");
console.log(_const.a); // undefined</code></pre>
<h2>结语</h2>
<p><code>export default</code>配合<code>babel-plugin-add-module-exports</code>给了我们很好的开发体验,但是还是要遵守<code>export default</code> 的基本规则:<br>虽然<code>es6 export default</code> 导出的内容有工具帮你处理,但是 <code>es6 import</code> 不是解构语法。需要注意的是,在引入一个有默认输出的模块时,这时<code>import</code>命令后面,不使用大括号,不要对引入的内容进行解构。</p>
<p>帮助链接:</p>
<blockquote><a href="https://link.segmentfault.com/?enc=uImRLicLkQzjaykQCnXrrw%3D%3D.n58UMjiB70ax5DeF2I3iJyQKtC6tIBXNVlGkFtJDwKd4t05ZWsO5YjkhdtYG7Q0VuVO0m53IVfx0rn5UVAlajg%3D%3D" rel="nofollow">关于 import、require、export、module.exports</a></blockquote>
redux之redux-thunk和redux-saga
https://segmentfault.com/a/1190000021813673
2020-02-22T22:10:44+08:00
2020-02-22T22:10:44+08:00
了不起的小六先生
https://segmentfault.com/u/liaobuqidexiaoliuxiansheng
0
<p>本文阅读前要有一定redux基础</p>
<p>redux作为状态管理仓库,在我们前端应用中发挥着非常重要的作用,先放一张官方redux flow图片<br><img src="/img/bVbDGSI" alt="image.png" title="image.png"><br>使用middleWare背景:我们知道redux中数据流是同步的,不支持异步action更新或获取数据,但是在实际项目中异步请求数据绝对是高频出现,并且可以说占据了9成以上的业务场景(初始数据列表、获取商品信息、添加购物车等等),因此redux中间件诞生了</p>
<hr>
<h3> redux-thunk异步action示例:</h3>
<pre><code>// store.js
import {createStore,applyMiddleware,compose} from 'redux';
import thunk from 'redux-thunk';
import reducers from './reducer.js';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 用到了chrome的redux插件,所以这里用到增强函数compose
export default createStore(reducers,composeEnhancers(applyMiddleware(thunk)));
// 1、利用applyMiddleware使用中间件
// 2、createStore只接受两个参数,所以如果要引用多个中间件applyMiddleware支持传入数组</code></pre>
<pre><code>// actionCreators.js
export const getTodoListAction => (value) => ({
type:GET_LIST,
value
})
export const getTodoList = () => (dispatch) => axios('api/getList').then( data => {
store.dispatch(getTodoListAction(data))
})
// thunk中间件使得action返回一个函数而不是对象,从而可以监听延迟action的派遣或者再满足特定条件下(接口数据返回)再进行派遣</code></pre>
<pre><code>// react组件 index.js
import { getTodoList } from './store/actionCreators';
import store from './store.js';
componentDidMount(){
const action = getTodoList();
store.dispatch(action);
} </code></pre>
<h3>redux-saga异步action示例:</h3>
<pre><code>// store.js
import {createStore,applyMiddleware,compose} from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducers from './reducer.js';
import sage from './sage.js'; // 要引入saga的执行方法(其实就是异步action的方法)
const sagaMiddleware = createSagaMiddleware(); // 先创建中间件
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 用到了chrome的redux插件,所以这里用到增强函数compose
export default createStore(reducers,composeEnhancers(applyMiddleware(sagaMiddleware)));
sagaMiddleware.run(sage); // run方法执行中间件</code></pre>
<pre><code>// saga.js
// 返回的是generator函数
import { takeEvery , put} from 'redux-saga';
import superagent from 'superagent';
import { getTodoList } from './actionCreators.js';
import { GET_MY_LIST } from './actionType.js';
// mySaga方法会监听相应type类型的action,然后做异步处理延缓派遣
function* mySaga(){
yield takeEvery(GET_MY_LIST,getMyList); // saga会在这里拦截相应type的action并做异步处理
}
function* getMyList(){
const res = yield superagent.get('/api/url');
const action = getTodoList(res);
yield put(action); // 发起相应action,类似于dispatch
}
export default mySaga;
// 这里做个扩展,我们知道generator中多个yield会顺序执行,但我们需要多个并行互不影响的异步操作怎么办? 很简单,引入all
import { all } from 'redux-saga';
export default function* rootSaga(){
yield all([
fun1(), // generator异步方法1
fun2() // generator异步方法2
])
}</code></pre>
<pre><code>// react组件index.js
import { getMyListAction } from './actionCreators.js';
import store from './store.js';
componentDidMount(){
const action = getMyListAction();
store.dispatch(action);
}</code></pre>
<pre><code>// actionCreators.js
import { GET_MY_LIST } from './actionType.js';
export const getMyListAction = () => ({
type:GET_MY_LIST,
})
export const getTodoList = (value)=> ({
type:GET_MY_LIST,
value
})</code></pre>
happy pack 原理解析
https://segmentfault.com/a/1190000021037299
2019-11-18T02:35:56+08:00
2019-11-18T02:35:56+08:00
csywweb
https://segmentfault.com/u/csywweb
15
<h2>前言</h2>
<p>当 webpack 打包速度很慢的时候,我们想过很多办法去优化打包速度,<strong><a href="#">happypack</a></strong> 就是一个用来加速打包的插件。</p>
<p>本质上, <strong>happypack</strong> 是用通过 js 的多进程来实现打包加速,需要注意的是,创建子进程和子进程和主进程之间通信也是有开销的,当你的 <strong>loader</strong> 很慢的时候,可以加上 <strong>happypack</strong>,否则,可能会编译的更慢!</p>
<h2>happypack 加载入口</h2>
<p><img src="/img/bVbAqQW" alt="HappyPack_Workflow.png" title="HappyPack_Workflow.png"><br>HappyPack位于webpack和您的主要源文件(例如JS源)之间,在该文件中,大量的加载程序发生转换。每次webpack解析模块时,HappyPack都会获取该模块及其所有依赖项,并将这些文件分发到多个工作程序“线程”。</p>
<h3>webpack 的配置</h3>
<pre><code>var HappyPack = require('happypack');
var happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
...
module: {
rules: [
{
test: /\.(js?|tsx?|ts?)$/,
include: [
path.resolve(__dirname, 'src'),
],
use: [
{
loader: 'happypack/loader?id=happyBabel',
},
],
},
...
plugins: [
new HappyPack({
id: 'happyBabel',
loaders: ['babel-loader'],
threadPool: happyThreadPool,
verbose: true,
})
]
...</code></pre>
<p>可以看到 happypack 是通过 <strong><a href="#">loader</a></strong> 调用 <strong><a href="#">plugin</a></strong> 来打成插件的目的。<strong>loader</strong> 指向 <strong>HappyLoader.js</strong> , loader 执行的时候,根据 <strong>?</strong> 后面的 <strong>id</strong> 来找到对应的插件。</p>
<h2>happypack 解析过程</h2>
<h3>参数初始化</h3>
<p>在我们指定的 happypack loader 加载之前,webpack 会根据 option 初始化配置信息之后再去执行 loader。</p>
<pre><code>// HappyPlugin.js
this.name = 'HappyPack';
this.state = {
loaders: [],
baseLoaderRequest: '',
foregroundThreadPool: null,
verbose: false,
debug: false,
};
this.config = OptionParser(userConfig, {
id: { type: 'string', default: '1' },
compilerId: { type: 'string', default: 'default' },
tempDir: { deprecated: true },
threads: { type: 'number', default: 3 },
threadPool: { type: 'object', default: null },
verbose: { type: 'boolean', default: true },
verboseWhenProfiling: { type: 'boolean', default: false },
debug: { type: 'boolean', default: process.env.DEBUG === '1' },
enabled: { deprecated: true },
// we don't want this to be documented / exposed since it's an
// implementation detail + not having it on means a bug, but we're making it
// configurable for testing purposes
bufferedMessaging: { type: 'boolean', default: process.platform === 'win32' },
loaders: {
validate: function(value) {
if (!Array.isArray(value)) {
return 'Loaders must be an array!';
}
else if (value.length === 0) {
return 'You must specify at least one loader!';
}
else if (value.some(function(loader) {
return typeof loader !== 'string' && !loader.path && !loader.loader;
})) {
return 'Loader must have a @path or @loader property or be a string.'
}
},
}
}, "HappyPack");
this.id = this.config.id;</code></pre>
<p><strong> OptionParser </strong> 方法是一个参数校验,初始化的方法<br>参数说明:</p>
<ul>
<li>id: 在配置文件中设置的与 loader 关联的 id 首先会设置到实例上,为了后续 loader 与 plugin 能进行一对一匹配,在 <strong> * HappyLoader.js</strong> 中有相对应的判断</li>
<li>compilerId: 用于查找当前 webpack compioler 对象的上下文,默认 default</li>
<li>tmpDir: 存放打包缓存文件的位置</li>
<li>verbose: 是否输出过程日志</li>
<li>debug: 是否输出父子进程之间的 debug 信息</li>
<li>bufferedMessaging: 在 windows 系统中是否是通过 buffered 传输</li>
<li>loaders: 因为配置中文件的处理 loader 都指向了 happypack 提供的 loader ,这里配置的对应文件实际需要运行的 loader</li>
</ul>
<h3>进程池创建</h3>
<pre><code>// HappyPlugin.prototype.apply
this.threadPool = this.config.threadPool || HappyThreadPool({
id: this.id,
size: this.config.threads,
verbose: this.state.verbose,
debug: this.state.debug,
bufferedMessaging: this.config.bufferedMessaging,
});</code></pre>
<h4>rpcHandler</h4>
<p>在创建子进程之前会生成一个 rpcHandler 对象, rpcHandler 是 <strong> HappyRPCHandler </strong> 的实例<br><em>HappyRPCHandler.js</em></p>
<pre><code>function HappyRPCHandler() {
this.activeLoaders = new SharedPtrMap();
this.activeCompilers = new SharedPtrMap();
}</code></pre>
<p><em>SharedPtrMap</em> 给 <em>activeLoaders</em> 和 <em>activeCompilers</em> 定义了set、get 和 delete 方法,用于其他方法赋值</p>
<pre><code>HappyRPCHandler.prototype.registerActiveCompiler = function(id, compiler) {
this.activeCompilers.set(id || DEFAULT_COMPILER_ID, compiler);
};
HappyRPCHandler.prototype.unregisterActiveCompiler = function(id) {
this.activeCompilers.delete(id || DEFAULT_COMPILER_ID);
};
HappyRPCHandler.prototype.registerActiveLoader = function(id, instance) {
this.activeLoaders.set(id || DEFAULT_LOADER_ID, instance);
};
HappyRPCHandler.prototype.unregisterActiveLoader = function(id) {
this.activeLoaders.delete(id || DEFAULT_LOADER_ID);
};</code></pre>
<p><strong>rpcHandler</strong> 的主要作用是: 绑定当前运行的 loader 与 compiler ,同时在文件中,针对 loader 与 compiler 定义调用接口:</p>
<pre><code>COMPILER_RPCs = {
resolve: function(compiler, payload, done) {
var resolver = compiler.resolvers.normal;
var resolve = compiler.resolvers.normal.resolve;
...
resolve.call(resolver, payload.context, payload.context, payload.resource, done);
},
};</code></pre>
<pre><code>LOADER_RPCS = {
emitWarning: function(loader, payload) {
loader.emitWarning(payload.message);
},
emitError: function(loader, payload) {
loader.emitError(payload.message);
},
addDependency: function(loader, payload) {
loader.addDependency(payload.file);
},
addContextDependency: function(loader, payload) {
loader.addContextDependency(payload.file);
},
};</code></pre>
<h4>创建子进程</h4>
<p><em>HappyThreadPool.js</em></p>
<pre><code> var threads = createThreads(config.size, rpcHandler, {
id: config.id,
verbose: config.verbose,
debug: config.debug,
buffered: config.hasOwnProperty('bufferedMessaging') ?
config.bufferedMessaging :
process.platform === 'win32',
});
...
...
function createThreads(count, rpcHandler, config) {
var set = []
for (var threadId = 0; threadId < count; ++threadId) {
var fullThreadId = config.id ? [ config.id, threadId ].join(':') : threadId;
set.push(HappyThread(fullThreadId, rpcHandler, config));
}
return set;
}</code></pre>
<p><strong>threads</strong> 为 <strong>HappyThread</strong> 返回的操作子进程的对象</p>
<p><em>HappyThread.js</em></p>
<pre><code>var WORKER_BIN = path.resolve(__dirname, 'HappyWorkerChannel.js');
....
return {
open: function(onReady) {
var emitReady = Once(onReady);
fd = fork(WORKER_BIN, [id, JSON.stringify({ buffered: config.buffered })], {
// Do not pass through any arguments that were passed to the main
// process (webpack or node) because they could have unwanted
// side-effects, see issue #47
execArgv: [],
});
fd.on('error', throwError);
fd.on('exit', function(exitCode) {
if (exitCode !== 0) {
emitReady('HappyPack: worker exited abnormally with code ' + exitCode);
}
});
fd.on('message', function(message) {
});
},
configure: function(compilerId, compilerOptions, done) {
},
/**
* @param {Object} params
* @param {String} params.compiledPath
* @param {Object} params.loaderContext
*
* @param {Function} done
*/
compile: function(params, done) {
},
isOpen: function() {
return !!fd;
},
close: function() {
},
};
}</code></pre>
<p>到目前为止,子进程已经创建完成。</p>
<h3>初始化</h3>
<p>我们回到 <strong>HappyPlugin.js</strong>,</p>
<pre><code>compiler.plugin('watch-run', function(_, done) {
if (engageWatchMode() === fnOnce.ALREADY_CALLED) {
done();
}
else {
that.start(compiler, done);
}
});
compiler.plugin('run', that.start.bind(that));
</code></pre>
<p>看到这里在 <strong>run</strong> 和 <strong>watch-tun</strong> 两个 <strong><a href="#">钩子</a></strong> 中调用了 this.start 进行初始化.</p>
<pre><code>HappyPlugin.prototype.start = function(compiler, done) {
var that = this;
...
async.series([
function resolveLoaders(callback) {},
function launchAndConfigureThreads(callback) {},
function announceReadiness(callback) {}
], done);
};</code></pre>
<p>start函数通过 <strong><a href="#">async.series</a></strong> 将整个过程串联起来。</p>
<p>1) resolveLoaders<br>loader 解析,把 loaders 和 baseLoaderRequest 塞到 this.state 里面</p>
<pre><code>function resolveLoaders(callback) {
var normalLoaders = that.config.loaders.reduce(function(list, entry) {
return list.concat(WebpackUtils.normalizeLoader(entry));
}, []);
var loaderPaths = normalLoaders.map(function(loader) {
return loader.path;
});
WebpackUtils.resolveLoaders(compiler, loaderPaths, function(err, resolvedPaths) {
if (err) return callback(err);
var withResolvedPaths = normalLoaders.map(function(loader, index) {
var resolvedPath = resolvedPaths[index];
return Object.assign({}, loader, {
path: resolvedPath,
request: loader.query ? (loader.path + loader.query) : loader.path
})
})
that.state.loaders = withResolvedPaths;
that.state.baseLoaderRequest = withResolvedPaths.map(function(loader) {
return loader.path + (loader.query || '');
}).join('!');
callback();
});
},</code></pre>
<p>2) launchAndConfigureThreads<br>启动和初始化进程</p>
<pre><code>that.threadPool.start(that.config.compilerId, compiler, serializedOptions, callback);</code></pre>
<p>这里调用到了 <em>HappyThreadPool.js</em> 里的 <strong>start</strong> 方法<br>that.threadPool 是进程池创建得到的对象<br>参数说明:</p>
<ul>
<li>that.config.compilerId: 用于查找当前 webpack compioler 对象的上下文,默认 default</li>
<li>compiler: 当前上下文compiler对象</li>
<li>serializedOptions: webpack的入参,例如 webpack.common.js 的参数</li>
<li>callback: 可以理解为下一步,<strong>async</strong> 库的使用方法</li>
</ul>
<p>** 第一步:registerActiveCompiler: <code>RPCHandler</code> 绑定 <code>compiler</code></p>
<pre><code>rpcHandler.registerActiveCompiler(compilerId, compiler);</code></pre>
<p>** 第二步: 找到当前没打开的子进程,调用 <code>open</code> fork 一份子进程<br><em>HappyThreadPool.js</em></p>
<pre><code>async.parallel(threads.filter(not(send('isOpen'))).map(get('open'))</code></pre>
<p><em>HappyThread.js</em></p>
<pre><code>open: function(onReady) {
var emitReady = Once(onReady);
fd = fork(WORKER_BIN, [id, JSON.stringify({ buffered: config.buffered })], {
execArgv: [],
});
fd.on('error', throwError);
fd.on('exit', function(exitCode) {
if (exitCode !== 0) {
emitReady('HappyPack: worker exited abnormally with code ' + exitCode);
}
});
fd.on('message', function(message) {
// message 判断
});
},</code></pre>
<p>子进程运行文件 <code>WORKER_BIN</code> 对应的是 <code>HappyWorkerChannel.js</code></p>
<pre><code>var HappyWorker = require('./HappyWorker');
if (process.argv[1] === __filename) {
startAsWorker();
}
function startAsWorker() {
HappyWorkerChannel(String(process.argv[2]), process);
}
function HappyWorkerChannel(id, fd, config) {
var fakeCompilers = {};
var workers = {};
fd.on('message', accept)
send({ name: 'READY' });
function accept(message) {
// 省略函数内容
}
}</code></pre>
<p>通过 <em>fd.on('message', accept)</em> 来监听主进程发送过来的消息</p>
<p>** 第三步: 子进程都生成之后,调用 <code>configure</code> 初始化</p>
<pre><code>async.parallel(threads.map(function(thread) {
return function(callback) {
thread.configure(compilerId, compilerOptions, callback);
}
}), done);</code></pre>
<p>同样的,来到了 <em>HappyThread.js</em> 的 <strong> configure </strong> 方法,</p>
<pre><code>configure: function(compilerId, compilerOptions, done) {
var messageId = generateMessageId();
callbacks[messageId] = done;
send({
id: messageId,
name: 'CONFIGURE',
data: {
compilerId: compilerId,
compilerOptions: compilerOptions
}
});
},</code></pre>
<p>给子进程发送了一条 <code>CONFIGURE</code> 消息,<em>HappyWorkerChannel</em> 接收到做了如下处理</p>
<pre><code>findOrCreateFakeCompiler(message.data.compilerId)
.configure(JSONSerializer.deserialize(message.data.compilerOptions));
send({
id: message.id,
name: 'CONFIGURE_DONE'
});</code></pre>
<p><strong>findOrCreateFakeCompiler</strong> 方法给 <code>workers</code> 和 <code>fakeCompiler</code> 赋值</p>
<ul>
<li>fakeCompilers 是根据接收到的 messageId 作为key,来生成对应的模拟编译环境, 可以理解为为了模拟执行 loader 而模拟出来的上下文</li>
<li>workers 是对应的子进程</li>
</ul>
<p>同时调用 <code>fakeCompiler</code> 的 configure 来初始化默认的 webpack 配置和编译上下文。</p>
<p>至此,happyPack 的初始化工作全部做完。</p>
<h3>开始编译</h3>
<h4>入口</h4>
<p>在 webpack 流程中,在源码文件完成内容读取之后,开始进入到 loader 的编译执行阶段,这时 HappyLoader 作为编译逻辑入口,开始进行编译流程。</p>
<p><em>HappyLoader</em></p>
<pre><code>function HappyLoader(sourceCode, sourceMap) {
var query, compilerId, loaderId, remoteLoaderId, happyPlugin;
var callback = this.async();
var pluginList = locatePluginList(this);
query = loaderUtils.getOptions(this) || {};
compilerId = query.compilerId || DEFAULT_COMPILER_ID;
loaderId = query.id || DEFAULT_LOADER_ID;
remoteLoaderId = 'Loader::' + compilerId + loaderId.toString() + ':' + this.resource;
happyPlugin = pluginList.filter(isHappy(loaderId))[0];
happyPlugin.compile(this, addWebpack2Context(this, {
compilerId: compilerId,
context: this.context,
minimize: this.minimize,
remoteLoaderId: remoteLoaderId,
request: happyPlugin.generateRequest(this.resource),
resource: this.resource,
resourcePath: this.resourcePath,
resourceQuery: this.resourceQuery,
sourceCode: sourceCode,
sourceMap: sourceMap,
target: this.target,
useSourceMap: this._module.useSourceMap,
}), callback);
}</code></pre>
<p>根据 loader 配置 <code>?</code>后面的参数找到对应的插件。<br>同时将原本的 <strong>loaderContext(this指向)</strong> 对象的一些参数例如 <strong>this.resource</strong>、<strong>this.resourcePath</strong> 等透传到 <strong>HappyPlugin.compile</strong> 方法进行编译</p>
<h4>编译</h4>
<p>编译的起始位置在 <strong>HappyPlugin</strong> 的 <strong>compile</strong> 方法</p>
<p><em>HappyPlugin.js</em></p>
<pre><code>HappyPlugin.prototype.compile = function(loader, loaderContext, done) {
var threadPool = this.state.foregroundThreadPool || this.threadPool;
threadPool.compile(loaderContext.remoteLoaderId, loader, {
loaders: this.state.loaders,
loaderContext: loaderContext,
}, function(err, result) {
if (err) {
done(ErrorSerializer.deserialize(err));
}
else {
done(null,
result.compiledSource || '',
SourceMapSerializer.deserialize(result.compiledMap)
);
}
});
};</code></pre>
<p>这里调用了进程池的 compile<br><em>HappyThreadPool.js</em></p>
<pre><code>compile: function(loaderId, loader, params, done) {
var worker = getThread();
rpcHandler.registerActiveLoader(loaderId, loader);
worker.compile(params, function(message) {
rpcHandler.unregisterActiveLoader(loaderId);
done(message.error, message.data);
});
},</code></pre>
<p>这里第一步是给 <strong>rpcHandler</strong> 注册了当前的 loader 信息<br>第二步 通过<strong>getThread</strong>找到了对应的进程,调用了 <em>HappyThread.js</em></p>
<pre><code>//getThread
function RoundRobinThreadPool(threads) {
var lastThreadId = 0;
return function getThread() {
var threadId = lastThreadId;
lastThreadId++;
if (lastThreadId >= threads.length) {
lastThreadId = 0;
}
return threads[threadId];
}
}</code></pre>
<p>RoundRobinThreadPool 这里的递增取对应进程很巧妙<br>最终是由 <em>HappyThread.js</em> 给子进程发了一个 <code>COMPILE</code> 消息</p>
<pre><code>// HappyThread.js
/**
* @param {Object} params
* @param {String} params.compiledPath
* @param {Object} params.loaderContext
*
* @param {Function} done
*/
compile: function(params, done) {
var messageId = generateMessageId();
callbacks[messageId] = done;
send({
id: messageId,
name: 'COMPILE',
data: params,
});
},</code></pre>
<p>这里的 messageId 是个从 0 开始的递增数字,完成回调方法的存储注册,方便完成编译之后找到回调方法传递信息回主进程。同时在 thread 这一层,也是将参数透传给子进程执行编译。</p>
<p>子进程收到消息<br><em>HappyWorkerChannel.js</em></p>
<pre><code>COMPILE: function(message) {
getWorker(message.data.loaderContext.compilerId)
.compile(message.data, function(err, data) {
send({
id: message.id,
name: 'COMPILED',
error: err,
data: data
});
});
},</code></pre>
<p>收到消息后,调用 <strong>worker.compile</strong> <br><em>HappyWorker.js</em></p>
<pre><code>/**
* @param {Object} params
* @param {Object} params.loaderContext
* @param {String} params.loaderContext.sourceCode
* @param {?String|?Object} params.loaderContext.sourceMap
* @param {Array.<String>} params.loaders
* @param {Function} done
*/
HappyWorker.prototype.compile = function(params, done) {
assert(typeof params.loaderContext.resourcePath === 'string',
"ArgumentError: expected params.sourcePath to contain path to the source file."
);
assert(Array.isArray(params.loaders),
"ArgumentError: expected params.loaders to contain a list of loaders."
);
applyLoaders({
compiler: this._compiler,
loaders: params.loaders,
loaderContext: params.loaderContext,
}, params.loaderContext.sourceCode, params.loaderContext.sourceMap, function(err, source, sourceMap) {
if (err) {
done(ErrorSerializer.serialize(err))
}
else {
done(null, {
compiledSource: source,
compiledMap: SourceMapSerializer.serialize(sourceMap)
});
}
});
};</code></pre>
<p>在 HappyWorker.js 中的<code>compile</code>方法中,调用<code>applyLoaders</code>进行 loader 方法执行。<code>applyLoaders</code>是<code>happypack</code>中对<code>webpack</code>中 loader 执行过程进行模拟,对应 NormalModuleMixin.js 中的<a href="https://link.segmentfault.com/?enc=1WduDYstbXNILmyewbEzWA%3D%3D.Vyr2Scs7qJZhBAT3WpWO2Jq3hFvNWwNuMdaNqxyqh%2BmC1eGpHNlzgA9lhoHT0BzeFWqQjg%2FXAzChyb%2BF3OURXlXcLloGVxco4Ykehv%2BxvlE%3D" rel="nofollow"><code>doBuild</code></a>方法。完成对文件的字符串处理编译。</p>
<p>根据<code>err</code>判断是否成功。如果判断成功,则将对应文件的编译后内容写入之前传递进来的<code>compiledPath</code>,反之,则会把错误内容写入。</p>
<p>关于 <strong>loader</strong> 内部的执行机制可以点<a href="#">这里</a></p>
<h4>编译结束</h4>
<p>当 webpack 整体编译流程结束后, happypack 开始进行一些善后工作</p>
<pre><code>// HappyPlugin.js
compiler.plugin('done', that.stop.bind(that));
HappyPlugin.prototype.stop = function() {
if (this.config.cache) {
this.cache.save();
}
this.threadPool.stop();
};
</code></pre>
<h4>整体流程</h4>
<p>结合webpack的编译,整体流程可以参考下图<br><img src="/img/bVbATCt" alt="happypack流程.png" title="happypack流程.png"></p>
<h2>后记</h2>
<p>happypack 源码阅读需要对 webpack 有一定的了解,阅读难度主要在于webpack相关的api的了解。有兴趣的同学可以一起讨论</p>
typescript 改造二三事
https://segmentfault.com/a/1190000020605016
2019-10-07T14:08:23+08:00
2019-10-07T14:08:23+08:00
csywweb
https://segmentfault.com/u/csywweb
3
<h2>故事背景</h2>
<blockquote>最近项目中遇到了这样的问题,接手了一段代码,代码中定义了一个对象 item ,接下来的方法根据 item 内部的某些字段进行一顿 format 操作,问题的关键在于,我接手的时候没有人给我讲这个 item 对象内部都有什么字段,表达什么含义,当代码量足够大的时候,维护性就成了问题,所以就有了在项目中引入 typescript 的故事</blockquote>
<h2>安装 typescript</h2>
<p>项目现状:node版本 8.x babel6.x<br>看了一下ts <a href="https://link.segmentfault.com/?enc=IIHyG7yoTLTJudpPi5B9Pg%3D%3D.ZLou9ST0A%2F%2F%2FaeHgMSPuyD7XV0wjQrr3abWvMfjKge11J%2F9gvqY7K9oPskF6YzWB6qW00jUGT0MFWBgGQPe%2Bpw%3D%3D" rel="nofollow">官方文档</a>,引入ts只需要安装<strong>typescript</strong> <strong>@types/react</strong> <strong>@types/react-dom</strong> 再配置一下<strong>webpack</strong>的配置就ok</p>
<h2>安装配套babel插件和预设</h2>
<p>首先我们需要添加一个 <strong>@babel/preset-typescript</strong> 在 babel 配置的 preset 里,安装一下</p>
<pre><code>yarn add @babel/preset-typescript --save</code></pre>
<p>先把他如此粗暴的添加到配置中去,运行起来看看会发生什么</p>
<pre><code>presets: ['react', 'es2015', 'stage-0', '@babel/preset-typescript'],</code></pre>
<pre><code>npm run dev</code></pre>
<p>let's do it~</p>
<p><img src="/img/bVbyCrA?w=1050&h=238" alt="clipboard.png" title="clipboard.png"></p>
<p>发现 @babel/preset-typescript 需要babel7.X 支持,我们升级下 babel<br>项目当先babel相关设置</p>
<pre><code>"babel-cli": "^6.24.1",
"babel-core": "^6.25.0",
"babel-eslint": "^8.0.3",
"babel-jest": "^21.0.2",
"babel-loader": "^7.1.1",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-import": "^1.11.2",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-react-jsx-source": "^6.22.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-plugin-zent": "^2.0.0-next.5",
"babel-preset-env": "^1.6.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.24.1",</code></pre>
<p>我们更新一波,更新后的配置为</p>
<pre><code>"@babel/cli": "^7.6.2",
"@babel/core": "^7.6.2",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.6.0",
"@babel/plugin-proposal-export-default-from": "^7.5.2",
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-modules-commonjs": "^7.6.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.6.2",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.6.0",
"@babel/register": "^7.6.2",
"@babel/runtime": "^7.6.2",
"babel-loader": "^8.0.6",</code></pre>
<p>调整项目原先babel配置<br>具体可以参考 <a href="https://link.segmentfault.com/?enc=FBpsgosd%2FwJS2DPAKsAmDg%3D%3D.i6UOmDE%2BGN7OxOmJrgFqsl2SyrG%2F2dwGtHnCC2zDuVjw%2FK2x9CtnV1aoFe6%2FQZP%2B" rel="nofollow">babel7 升级指南</a></p>
<p>去掉原先配置中的 <strong>es2015</strong> <strong>stage-0</strong> 的preset,替换为 @babel/preset-env<br>修改多个仓库 <strong>babel-plugin-import</strong> 的使用方法 (多个分开调用import插件)<br>修改装饰器插件 <strong>transform-decorators-legacy</strong> => <strong>@babel/plugin-proposal-decorators</strong><br>支持类属性的转化 <strong>@babel/plugin-proposal-class-properties</strong><br>支持对象使用解构 <strong>@babel/proposal-object-rest-spread</strong><br>支持动态导入文件 <strong>@babel/plugin-syntax-dynamic-import</strong><br>转换为commonjs包 <strong>@babel/plugin-transform-modules-commonjs</strong> (这一步是因为目前很多地方用js写的 export default {...} ts导入需要 import xx as xxx)</p>
<p>我的配置如下<br><img src="/img/bVbyCsP?w=1176&h=1302" alt="clipboard.png" title="clipboard.png"></p>
<p>tsconfig</p>
<pre><code>{
"include": ["src/**/*"],
"exclude": ["node_modules/**"]
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": [
"dom",
"es2017",
"es2018.promise"
],
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"allowJs": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"sourceMap": true,
}
}</code></pre>
<p>ok 目前来看 问题都已经解决了,我们再次 run dev 试试看</p>
<p><img src="/img/bVbyCs7?w=840&h=222" alt="clipboard.png" title="clipboard.png"></p>
<p>Perfect! 运行没有问题, 时间还比之前短了 10000 毫秒的样子。</p>
<h2>ts测试</h2>
<p>新建一个ts</p>
<pre><code>import React, { Component } from 'react';
import { Button } from 'zent';
interface IExampleComponentProps {
text?: string;
}
export default class ExampleComponent extends Component<IExampleComponentProps> {
render() {
const { text } = this.props;
return (
<div className="example-component">
<h1>Example Component: {text}</h1>
<Button>123</Button>
</div>
);
}
}
</code></pre>
<p>完美运行!</p>
<h2>结语</h2>
<p>这篇博客其实是想记录一下我的 typescript 升级过程,希望可以帮助到同样在升级 typescript 的你们</p>
nest.js、egg.js、midway,express、koa的区别
https://segmentfault.com/a/1190000020510357
2019-09-27T10:31:56+08:00
2019-09-27T10:31:56+08:00
dahaowenxxx
https://segmentfault.com/u/dahaowenxxx
9
<h2><strong>前言</strong></h2>
<p> <strong>随着nest.js框架逐渐火起来,对于没有接触过nest的小伙伴可能会疑惑nest是什么?在你了解了nest.js是什么的前提下,你可能会疑惑nest.js与传统的koa、express有什么区别?针对这两个问题,结合了网上的一些文章,做一个简单的总结。</strong><br> </p>
<h2>nest是什么?</h2>
<p> <img src="/img/bVbydOs" alt="clipboard.png" title="clipboard.png"><br> </p>
<ul>
<li>nest的定义是一个渐进式的Node.js框架,用于构建高效,可靠和可扩展的服务器端应用程序;不要问我为什么要放图,据说放图可以提高访问量。</li>
<li>nest是一个封装了node的有规范的框架,什么是有规范?意思是必须按照它制定的一套规则来写代码,否则程序就会无法运行。上手成本稍高一点,但是后期维护与扩展会很方便。</li>
<li>nest属于前端ts大趋势下深度使用注解特性并提供各种增强开发体验的框架,它提供了一套完整的解决方案,包含了认证、数据库、路由、http状态码、安全、配置、请求等开箱即用的技术。</li>
</ul>
<h2>nest.js与koa、express有什么区别?</h2>
<p> <br><img src="/img/bVbydOx" alt="clipboard.png" title="clipboard.png"></p>
<p> </p>
<ul>
<li>koa是一个拥有洋葱模型中间件的http处理库,一个请求,经过一系列的中间件,最后生成响应。Koa的大致实现原理:context上下文的保存和传递,中间件的管理和next方法的实现。</li>
<li>大概过程:我们koa常用的app.use方法就是将一系列中间件的方法存进了一个数组,app.listen底层用http.createServer(this.callback())进行封装,传进createServer的回调函数通过compose来处理中间件集合(就是递归遍历中间件数组的过程),通过req,res(这两个对象封装了node的原生http对象)创建上下文,并返回一个处理请求的函数(参数是上下文,中间件集合(类似一个链表))。</li>
</ul>
<h2>区别</h2>
<hr>
<ul>
<li><p>koa本身几乎不带任何其他的库,如果需要使用路由、错误处理、认证等功能需要自己安装并引入,什么都需要自己DIY。而使用nest.js时就不需要考虑这些问题了,依赖注入,pipe,guard,interceptor等机制,基本覆盖各种开发需要,开箱即用。</p></li>
<li><p>koa常常与express一起比较,都是偏底层的无态度的Web框架;而nest.js应该和egg.js,midway这种框架比较。egg.js源于阿里,它的企业级规范很多,但各方面能力极强,定位是框架的框架,其再上一层还有midwayjs,完全兼容ts,支持注解,一点都不输与Nest;此外国内做企业级NodeJS框架的越来多啦,比如刚出来的daruk等,用过JavaSpring 框架和 Angular的同学会发现NEST借鉴了两者很多的特性。</p></li>
<li>egg.js是在koa的基础上做了一层很好的面向大型企业级应用的框架封装,现在也有了非常好的TS特性支持。egg.js更多的是按照洋葱模型的开发方式,和AOP编程还是有点区别的。而nest配合TYPEORM可以在node下拥有不输SPRING的面向切面编程的体验~</li>
</ul>
<p> <strong>中小型项目推荐egg.js,上手快,概念易懂;大型项目不妨试试NEST.js+typeorm。如果你需要使用一些技术,例如认证、数据库等支持,那么你可能得去搜索、折腾一番,才能知道搭配哪个库,才行。希望大前端生态越来越繁荣!</strong></p>
关于 OKR 的一些方法论
https://segmentfault.com/a/1190000019691938
2019-07-08T08:26:37+08:00
2019-07-08T08:26:37+08:00
JerryC
https://segmentfault.com/u/jerryc
14
<blockquote>欢迎来我的博客阅读:<a href="https://link.segmentfault.com/?enc=viWeqdeXzRfWsVmC48DtYg%3D%3D.ENKabeyV1Q1DzlNR0%2BwDb8S3YpBvy6l3W0Jf%2FH60yN8224t1bhl6OFXFEhrQkAjX" rel="nofollow">关于 OKR 的一些方法论</a>
</blockquote>
<h2>前言</h2>
<p>OKR 是由前 Intel CEO,<a href="https://link.segmentfault.com/?enc=9nXD7fSt3D1QcMdf0pJlhA%3D%3D.71WTLGZE0r3OmY%2B44W5ZOJe5%2BTH9Kzp7s8Pe4Gin8BtulnADMtY3eAf0aLN3HgXgxG56Ck6iYBT5fUvPwc1mMEKqQ7%2FaUc5Ic5gLHtVC0OzGTbQKqN%2FfjScuX7JHU7DQ" rel="nofollow">安迪·葛洛夫</a> 构建的基本框架。</p>
<p>全称是:「Objective - Key Result」,既强调「目标」与衡量目标的「关键结果」</p>
<p>它是一套管理目标,让目标能落地的工具。 <br>它在硅谷科技公司中广为人知,并被世界各地的许多组织采用。 <br>它可以应用在组织中,也可以应用在个人的生活中,就像一种思考的模式。 </p>
<p>过去两年多的 OKR 实践,有一些体会。 <br>作为一个程序员,会自然的去寻找一个工具的最佳实践。 </p>
<p>于是,有了这篇文章。</p>
<h2>基本原理</h2>
<p>OKR 原理很简单。</p>
<p>要用好 OKR,我的理解,需要把握三个核心:</p>
<ul>
<li>目标</li>
<li>关键结果</li>
<li>过程管理</li>
</ul>
<p>它们分别回答了三个问题:</p>
<ul>
<li>应该做什么?</li>
<li>如何衡量做到了?</li>
<li>怎么落地?</li>
</ul>
<p>然后,思考 OKR,我认为还需要 cover 到两点:</p>
<ul>
<li>看得到的结果</li>
<li>说得出的价值</li>
</ul>
<p><strong>先抛一个不好的例子</strong></p>
<p>来自于我曾经定过的一个 OKR:</p>
<blockquote>
<p>O: 持续学习,提高自身战斗力</p>
<ul>
<li>KR1: CSS3 学习,阅读《CSS揭秘》产出阅读笔记。</li>
<li>KR2: 提高英文阅读能力,阅读《Security Your NodeJS Application》,产出一篇译文。</li>
<li>KR3: 对 Eggjs 或 Vue2 框架的源码进行解读,产出一篇源码解析。</li>
</ul>
</blockquote>
<p>我想先按顺序来讲讲「目标」、「关键结果」、「过程管理」。 <br>然后,再回过头来,看看这个例子为啥糟糕,可以怎样修改。</p>
<h4>目标 Objective</h4>
<blockquote>欲望让我们起航,但只有专注、规划和学习才能到达成功的彼岸</blockquote>
<h6>组织的诞生</h6>
<p>回到最初的时候,一个组织的诞生,绝大多数情况是由于一两个人的想法,然后以此为中心,开始聚拢更多有共同目标的人加入进来。</p>
<p>1976年,乔布斯成功说服沃茲尼克组装机器之后再拿去推销,他们的另一位朋友韦恩随后加入,三人在1976年4月1日成立苹果电脑公司。最初,Apple 仅仅是在卖组装电脑。</p>
<p>1996年,佩奇和布林在学校开始一项关于搜索的研究项目,开发出了搜索引擎 PageRank,后续改名 Google。最初,Google 仅仅是一个搜索引擎。</p>
<h6>组织的使命</h6>
<p>随着组织发展,人员壮大,这个能聚拢人的目标,必须要看得远。然后这个目标提升到用另一个词来形容 —「使命」。</p>
<p>Apple 的使命:「藉推广公平的资料使用惯例,建立用户对互联网之信任和信心」 <br>Google 的使命:「整合全球信息,使人人皆可访问和收益」 <br>阿里巴巴的使命:「让天下没有难做的生意」 <br>有赞的使命:「帮助每一位重视产品和服务的商家成功」 <br>以及最近我们团队的前端技术委员的使命:「以极致的技术高效支撑业务」 </p>
<p>使命描述一般都很简洁,并且容易记忆,像一句广告词,能深深的刻在脑海里。 <br>在工作中遇到问题的时候,这个使命就会一下子从脑海里蹦出来指引你找到答案。</p>
<p>其实在某个市场闲逛都有可能让你意识到这个市场有某个问题需要解决,而帮市场解决这个问题,就是一个使命。</p>
<h6>阶段性的目标</h6>
<p>为了一步步的达成「使命」,我们需要有目标。相对于使命,它粒度更小,且有时间限制。</p>
<p>所以,目标(Objective)应该:</p>
<ul>
<li>是阶段性的</li>
<li>是有优先级的</li>
<li>它需要能明确方向且鼓舞人心</li>
</ul>
<p>目标,是 OKR 中最重要,最需要想清楚,最首要确定的。 <br>在这里,需要回答:你有什么?你要什么?你能放弃什么?</p>
<h6>重要与紧急</h6>
<p>「鱼与熊掌不可得兼」,所以我们要有所取舍,事情排个优先级。 <br>「重要-紧急象限」是一个不错的指导工具,第一次看到它是在柯维《高效能人士的7个习惯》中的第三个习惯「要事第一」。</p>
<p><img src="/img/remote/1460000019691941?w=718&h=640" alt="重要-紧急" title="重要-紧急"></p>
<p>但在实施的过程中很有可能会遇到这样一个问题,紧急不重要的事情很紧急,总需要花时间和精力去处理它。然后重要不紧急的事情,会常常分配不到时间和精力。</p>
<p><strong>那么就让重要不紧急的事情也变得紧急起来。</strong></p>
<h6>目标需要自上而下的关联</h6>
<p>如果基础的商业问题没有解决,不论实现多少产品功能,团队整体的绩效一定会大打折扣。</p>
<p>在一个组织中,如果没有充分的理解上一层的目标,就很容易跑偏,没有真正在刀刃上使力,造成效率上的浪费。</p>
<p>达到充分的理解目标,是有难度的,对人的眼界、目标理解能力有很高的要求。这不仅仅是执行者责任,更是管理者的责任。</p>
<h4>关键结果 Key Result</h4>
<h6>衡量目标是否达成</h6>
<p>目标定下来了,如果不去执行和落地,那么它永远就只是一个目标。如何去衡量目标是否达到了,就是「关键结果」的任务。</p>
<p>在互联网产品中,通常可以量化的条件有:用户增长、用户激活、收入增长、产品性能、产品质量。</p>
<p>作为技术团队,会更加集中注意力在产品性能和产品质量上面,那么如何去找到这些方向的衡量指标,就要从实际出发了。</p>
<p>比如我们团队会用「质量系数 = BUG数/估时」,来感受一个项目的质量情况。虽然它会有些漏洞,但如果建立在互相信任的基础上,可以提供一定的参考价值。</p>
<h6>有些挑战性</h6>
<blockquote>当达到成结果的时候,我们应该是欢呼雀跃般的兴奋,而不是理所应当的淡定。</blockquote>
<p>定下一个关键结果之后,问一下自己,有多少信心可以完成。如果信心爆棚,就把目标定高些。如果信心不足,就把目标调低些。因为 OKR 的意义不在于完成目标,更重要的是它能挖掘团队以及个人的潜力。</p>
<p>如果觉得有必要的话,我们可以建立一个「信心指数」,用来帮助确定结果有足够的挑战性而不会让人失去信心。这个指数的开始值最好是 50%,然后通过过程管理来动态变更和追踪。</p>
<p>比如去年我负责的一个「优化微信小程序加载性能」项目中的关键结果:</p>
<ul><li>首屏加载时间 3s 内</li></ul>
<p>未优化的加载时间是 6s+,回顾当时对目标的信心指数的话,大概是 20%。虽然最后因为部分不可控因素没有达到这个目标,只能维持在 3s-4s 之间。但是这个过程中能让人费尽脑汁的找到各种方法,大幅的提升了除首屏加载以外其他方面的加载体验,这也是额外的收获。</p>
<p>作为管理者,你要清楚的知道哪些人推一推会有更高的产出,哪些人实际执行情况会出现问题,要能看得到看得懂目前组织的目标和进度,并与成员进行同步。</p>
<h4>过程管理</h4>
<p>OKR 定下来了,在期限内,就要奔着目标努力奋进。尽管中途发现问题,也尽量不要在中途更改 OKR,让我们尽力跑完计划的阶段再回来总结。我们也可以把时间维度切小,比如把年度切分为半年度,把半年度切分为季度。</p>
<p>并且,目标定下来之后,要经常定期共同回顾,共同看见。而不是定下来了,就放在那里,否则过程中团队发生了问题,成员遇到了困难,很大可能会不被看到。</p>
<p>比较好的形式是每周都一起坐下来看看,每个人分享一下成果,或者说说遇到的困难,看能不能得到其他人的帮助。这个过程,能及时的看到问题,也能让成员对目标有更强的参与感。</p>
<p>那么,OKR应该以什么方式来呈现?《OKR工作法》一书中提供了一种参考:「四象限呈现形式」</p>
<p><img src="/img/remote/1460000019691942" alt="四象限呈现" title="四象限呈现"></p>
<ul>
<li>第一象限:本周3-4件最重要的事情,并且进行优先级的排序</li>
<li>第二象限:把OKR内容罗列出来,关注和更新每一项KR的信心指数</li>
<li>第三象限:未来中长段时间中的计划,能让我们稍微看远一些。</li>
<li>第四象限:关注那些影响目标的关键因素会不会掉链子,例如团队状态,系统状态等。也可以用红蓝黄颜色表示出来。</li>
</ul>
<h2>回过头看看那个糟糕的例子</h2>
<p>糟糕的例子:</p>
<blockquote>
<p>O: 持续学习,提高自身战斗力</p>
<ul>
<li>KR1:CSS3 学习,阅读《CSS揭秘》 产出阅读笔记。</li>
<li>KR2:提高英文阅读能力,阅读《Security Your NodeJS Application》,产出一篇译文。</li>
<li>KR3: Vue2 框架的源码进行解读,产出一篇源码解析。</li>
</ul>
</blockquote>
<p>这个例子的背景是我 2017 年 4 月份加入到有赞,当时定的试用期内的其中一个目标。那时是我第一次认识和使用 OKR,只是单纯的把自身的技能提升计划给罗列了出来,看起来更像是一个 Todo List</p>
<p>现在回过头来看这一份 OKR,有不少问题:</p>
<ol>
<li>目标没有描述出来价值,提升了自身战斗力,然后呢?并没有自上而下的关联团队和组织的目标。所以从目标上,就已经走偏了。</li>
<li>假设目标正确,KR 也没有起到能衡量目标是否达成的作用。例如 KR1 完成了,对目标的推进,并没有说服力。</li>
<li>最后把 OKR 用成了 Todo List。</li>
</ol>
<p>那么我们从目标开始分析,当时作为一个新人加入到一个新的团队,对团队的技术栈和项目都很陌生,需要填补部分空白,快速上手。所以提升自身实力的底层诉求是:快速上手,胜任开发工作。</p>
<p>然后怎么衡量目的达到了呢?我们可以通过项目质量直接衡量,通过项目的熟悉程度来间接衡量。</p>
<p>修正后:</p>
<blockquote>
<p>O: 快速上手,以专业的姿态胜任开发工作。</p>
<ul>
<li>KR1: 质量系数平均在 0.3 以内。(质量系数 = BUG数/估时)</li>
<li>KR2: 代码评审评分平均 3.5 以上。(我们有 Code Review 机制,并且有评分环节)</li>
<li>KR3: 所参与项目评分在 4 以上。(项目也有评分环节)</li>
<li>KR4: 进行两次的项目分享。</li>
</ul>
</blockquote>
<p>那么如果达到这些关键结果,要通过学习框架,还是研究项目,还是熟悉业务,那就是根据实际迎刃而解的事情了。</p>
<h2>最后</h2>
<blockquote>凡事预则立,不预则废 ——《礼记·中庸》</blockquote>
<p>最后要注意的是,OKR 只是一个工具,当你有一个目标,它会给你一种落实目标的方法论。而如果一开始目标没有想清楚,想明白,那就很容易在错的路上越走越远。</p>
<p>每个团队都会有不同的风格,和不同的实际情况。理解方法和工具的原理,明白这么做是为了解决什么问题,然后再调整定制真正适合此时此刻的团队,才是最好的方法。</p>
BeautyWe.js 一套专注于微信小程序的开发范式
https://segmentfault.com/a/1190000019432720
2019-06-10T15:19:12+08:00
2019-06-10T15:19:12+08:00
JerryC
https://segmentfault.com/u/jerryc
16
<blockquote>欢迎来我的博客阅读:<a href="https://link.segmentfault.com/?enc=H4zApC%2F5I%2FuepdasKVV57g%3D%3D.rHa%2FdqFbkEkgSEGxfQU9xDVadGZ0UER9ny4VQhlUGEHH3rdG4XUkYJFZJe7IR2tuYjclzwR4T2irIqZI7bnFQQ%3D%3D" rel="nofollow">BeautyWe.js 一套专注于微信小程序的开发范式</a>
</blockquote>
<p><img src="/img/remote/1460000019432723?w=1504&h=905" alt="" title=""></p>
<blockquote>官网:<a href="https://link.segmentfault.com/?enc=NuoRENqgGEdlJEa8HEZWWQ%3D%3D.nHSauq3B61wVib1ig%2FjzHyFiMmP51Fpa4i2F%2BjD9EGs%3D" rel="nofollow">beautywejs.com</a> <br>Repo: <a href="https://link.segmentfault.com/?enc=qyE5omDgwFIcv3GbNPEWvQ%3D%3D.5FKqsoJglL2bLoSDyYv%2BnqZ4crGjM09Lkm3taun4ZrFpCeuEfld4WZMcwSrfMAWa" rel="nofollow">beautywe</a>
</blockquote>
<h2>一个简单的介绍</h2>
<p><strong>BeautyWe.js 是什么?</strong></p>
<p>它是一套专注于微信小程序的企业级开发范式,它的愿景是:</p>
<blockquote>让企业级的微信小程序项目中的代码,更加简单、漂亮。</blockquote>
<p><strong>为什么要这样命名呢?</strong></p>
<blockquote>Write <strong>beautiful</strong> code for <strong>we</strong>chat mini program by the <strong>beautiful</strong> <strong>we</strong>!</blockquote>
<p>「We」 既是我们的 <strong>We</strong>,也是微信的 <strong>We</strong>,Both beautiful!</p>
<p><strong>那么它有什么卖点呢?</strong></p>
<ol>
<li>专注于微信小程序环境,写原汁原味的微信小程序代码。</li>
<li>由于只专注于微信小程序,它的源码也很简单。</li>
<li>插件化的编程方式,让复杂逻辑更容易封装。</li>
<li>
<p>再加上一些配套设施:</p>
<ol>
<li>一些官方插件。</li>
<li>一套开箱即用,包含了工程化、项目规范以及微信小程序环境独特问题解决方案的框架。</li>
<li>一个CLI工具,帮你快速创建应用,页面,组件等。</li>
</ol>
</li>
</ol>
<p><strong>它由以下几部分组成:</strong></p>
<ul>
<li>
<strong>一个插件化的核心</strong> - <a href="https://link.segmentfault.com/?enc=hCPW6cW%2Bu0d4wJlDuIQGdg%3D%3D.VRXECJ%2B83bOC3pdVZbjm0%2BYdvb2Bpgt7FgzY%2BJqcDYEQicygjWcsMPJZhRQYISnR" rel="nofollow">BeautyWe Core</a> <br>对 App、Page 进行抽象和包装,保持传统微信小程序开发姿势,同时开放部分原生能力,让其具有「可插件化」的能力。</li>
<li>
<strong>一些官方插件</strong> — <a href="https://link.segmentfault.com/?enc=fY0Ux4fDVpemRUYNQ1twIg%3D%3D.CTXv8ky1YbWhG7YiVJj3QJMgj5BXD4gSu7DvqoAEncch56VX05Do2jUN2GaDlMino8BQt2foSaJYkc%2BEKyY%2FaQ%3D%3D" rel="nofollow">BeautyWe Plugins</a> <br>得益于 Core 的「可插件化」特性,封装复杂逻辑,实现可插拔。官方对于常见的需求提供了一些插件:如增强存储、发布/订阅、状态机、Logger、缓存策略等。</li>
<li>
<strong>一套开箱即用的项目框架</strong> - <a href="https://link.segmentfault.com/?enc=%2Fj8uX999kZD9e62xcvev5Q%3D%3D.R%2Bo%2F9Lykn4TXeQfGicRxq%2F9%2FTGgOWpLIQjYWGqvjyckqRWds4Rtfsy%2BU92axCgwxi1I19l3sBtKWQGZEk36DKYEGZBMmCbtTKdn5mdJphfKhsQAtzuyTC0YUgLXGfNNB" rel="nofollow">BeautyWe Framework</a> <br>描述了一种项目的组织形式,开箱即用,集成了 <code>BeautyWe Core</code> ,并且提供了如:全局窗口、开发规范、多环境开发、全局配置、NPM 等解决方案。</li>
<li>
<strong>一个CLI工具</strong> - <a href="https://link.segmentfault.com/?enc=Mt%2FMS4eqZjdw5VoXk1flTA%3D%3D.xX5FlLwz3vqXuUJKJkiu8r47gNA1dr0dw8ZKWKJDPiNBFsYF%2B9eReSqup%2FIc%2Bntohlm3jhJWia%2BaEwu6hyZXmECFSDW84WlQQplsr4ZaII8%3D" rel="nofollow">BeautyWe Cli</a> <br>提供快速创建应用、页面、插件,以及项目构建功能的命令行工具。并且还支持自定义的创建模板。</li>
</ul>
<h2>一个简单的例子</h2>
<p>下载</p>
<p><img src="/img/remote/1460000019709615" alt="" title=""></p>
<p>用 BeautyWe 包装你的应用</p>
<p><img src="/img/remote/1460000019709616" alt="" title=""></p>
<p>之后,你就能使用 BeautyWe Plugin 提供的能力了。</p>
<p><img src="/img/remote/1460000019709617" alt="" title=""></p>
<h2>开放原生App/Page,支持插件化</h2>
<p><code>new BtApp({...})</code> 的执行结果是对原生的应用进行包装,其中包含了「插件化」的处理,然后返回一个新的实例,这个实例适配原生的 <code>App()</code> 方法。</p>
<p>下面来讲讲「插件化」到底做了什么事情。</p>
<p>首先,插件化开放了原生 App 的四种能力:</p>
<ol>
<li>
<strong>Data 域</strong><br>把插件的 Data 域合并到原生 App 的 Data 域中,这一块很容易理解。</li>
<li>
<strong>原生钩子函数</strong><br>使原生钩子函数(如 <code>onShow</code>, <code>onLoad</code>)可插件化。让原生App与多个插件可以同时监听同一个钩子函数。如何工作的,下面会细说。</li>
<li>
<strong>事件钩子函数</strong><br>使事件钩子函数(与 view 层交互的钩子函数),尽管在实现上有一些差异,但是实现原理跟「原生钩子函数」一样的。</li>
<li>
<strong>自定义方法</strong><br>让插件能够给使用者提供 API。为了保证插件提供的 API 足够的优雅,支持当调用插件 API 的时候(如 event 插件 <code>this.event.on(...)</code>),API 方法内部仍然能通过 <code>this</code> 获取到原生实例。</li>
</ol>
<h5>钩子函数的插件化</h5>
<p>原生钩子函数,事件钩子函数我们统一称为「钩子函数」。</p>
<p>对于每一个钩子函数,内部是维护一个以 Series Promise 方式执行的执行队列。</p>
<p>以 <code>onShow</code> 为例,将会以这样的形式执行:</p>
<blockquote>native.onShow → pluginA.onShow → pluginB.onShow → ...</blockquote>
<p><strong>下面深入一下插件化的原理</strong>:</p>
<p><img src="/img/remote/1460000019709618" alt="beautywe pluggable" title="beautywe pluggable"></p>
<p>工作原理是这样的:</p>
<ol>
<li>经过 <code>new BtApp(...)</code> 包装,所有的钩子函数,都会有一个独立的执行队列,</li>
<li>首先会把原生的各个钩子函数 <code>push</code> 到对应的队列中。然后每 <code>use</code> 插件的时候,都会分解插件的钩子函数,往对应的队列 <code>push</code>。</li>
<li>当 <code>Native App</code>(原生)触发某个钩子的时候,<code>BtApp</code> 会以 Promise Series 的形式按循序执行对应队列里面的函数。</li>
<li>特殊的,<code>onLaunch</code> 和 <code>onLoad</code> 的执行队列中,会在队列顶部插入一个初始化的任务(<code>initialize</code>),它会以同步的方式按循序执行 <code>Initialize Queue</code> 里面的函数。这正是插件生命周期函数中的 <code>plugin.initialize</code>。</li>
</ol>
<p>这种设计能提供以下功能:</p>
<ol>
<li>可插件化。<br>只需要往对应钩子函数的事件队列中插入任务。</li>
<li>支持异步。<br>由于是以 Promise Series 方式运行的,其中一个任务返回一个 Promise,下一个任务会等待这个任务完成再开始。如果发生错误,会流转到原生的 <code>onError()</code> 中。</li>
<li>解决了微信小程序 <code>app.js</code> 中 <code>getApp() === undefinded </code>问题。<br>造成这个问题,本质是因为 <code>App()</code> 的时候,原生实例未创建。但是由于 Promise 在 event loop 中是一个微任务,被注册在下一次循环。所以 Promise 执行的时候 <code>App()</code> 早已经完成了。</li>
</ol>
<h2>一些官方插件</h2>
<p>BeautyWe 官方提供了一系列的插件:</p>
<ol>
<li>增强存储: Storage</li>
<li>数据列表:List Page</li>
<li>缓存策略:Cache</li>
<li>日志:Logger</li>
<li>事件发布/订阅:Event</li>
<li>状态机:Status</li>
</ol>
<p>它们的使用很简单,哪里需要插哪里。<br>由于篇幅的原因,下面挑几个比较有趣的来讲讲,更多的可以看看官方文档:<a href="https://link.segmentfault.com/?enc=netpIRi4UtSSvLZtoLrr8Q%3D%3D.32wV6hTKby8bqrRzPvZ27ud0rU0BdqCUN%2F1iAXytDhA%3D" rel="nofollow">BeautyWe</a></p>
<h3>增强存储 Storage</h3>
<p>该功能由 <a href="https://link.segmentfault.com/?enc=KL6Fr3rT4G9o9L5DDN63Aw%3D%3D.OAFvgEuk0%2FW0A11TYvf7UFhU%2Fek7OGq%2BioxCgaFWy8cGaFO16Cw4xC3TprzPdu3G" rel="nofollow">@beautywe/plugin-storage</a> 提供。</p>
<p>由于微信小程序原生的数据存储生命周期跟小程序本身一致,即除用户主动删除或超过一定时间被自动清理,否则数据都一直可用。</p>
<p>所以该插件在 <code>wx.getStorage/setStorage</code> 的基础上,提供了两种扩展能力:</p>
<ol>
<li>过期控制</li>
<li>版本隔离</li>
</ol>
<p><strong>一些简单的例子</strong></p>
<p>安装</p>
<pre><code class="javascript">import { BtApp } from '@beautywe/core';
import storage from '@beautywe/plugin-storage';
const app = new BtApp();
app.use(storage());</code></pre>
<p>过期控制</p>
<pre><code class="javascript">// 7天后过期
app.storage.set('name', 'jc', { expire: 7 });</code></pre>
<p>版本隔离</p>
<pre><code class="javascript">app.use({ appVersion: '0.0.1' });
app.set('name', 'jc');
// 返回 jc
app.get('name');
// 当版本更新后
app.use({ appVersion: '0.0.2' });
// 返回 undefined;
app.get('name');</code></pre>
<p>更多的查看 <a href="https://link.segmentfault.com/?enc=%2BrxCBIicAbTjire34y3prQ%3D%3D.7ul85y8xy3VRt2TSOWA%2Fd0QkelnjsEM8d%2BTCnbuGNm2X%2FA8iJUgxQ4MDRuxe3bYH" rel="nofollow">@beautywe/plugin-storage 官方文档</a></p>
<h3>数据列表 List Page</h3>
<p>对于十分常见的数据列表分页的业务场景,<code>@beautywe/plugin-listpage</code> 提供了一套打包方案:</p>
<ol>
<li>满足常用「数据列表分页」的业务场景</li>
<li>支持分页</li>
<li>支持多个数据列表</li>
<li>自动捕捉下拉重载:<code>onPullDownRefresh</code>
</li>
<li>自动捕捉上拉加载:<code>onReachBottom</code>
</li>
<li>自带请求锁,防止帕金森氏手抖用户</li>
<li>简单优雅的 API</li>
</ol>
<p>一个简单的例子:</p>
<pre><code class="javascript">import BeautyWe from '@beautywe/core';
import listpage from '@beautywe/plugin-listpage';
const page = new BeautyWe.BtPage();
// 使用 listpage 插件
page.use(listpage({
lists: [{
name: 'goods', // 数据名
pageSize: 20, // 每页多少条数据,默认 10
// 每一页的数据源,没次加载页面时,会调用函数,然后取返回的数据。
fetchPageData({ pageNo, pageSize }) {
// 获取数据
return API.getGoodsList({ pageNo, pageSize })
// 有时候,需要对服务器的数据进行处理,dataCooker 是你定义的函数。
.then((rawData) => dataCooker(rawData));
},
}],
enabledPullDownRefresh: true, // 开启下拉重载, 默认 false
enabledReachBottom: true, // 开启上拉加载, 默认 false
}));
// goods 数据会被加载到,goods 为上面定义的 name
// this.data.listPage.goods = {
// data: [...], // 视图层,通过该字段来获取具体的数据
// hasMore: true, // 视图层,通过该字段来识别是否有下一页
// currentPage: 1, // 视图层,通过该字段来识别当前第几页
// totalPage: undefined,
// }</code></pre>
<p>只需要告诉 <code>listpage</code> 如何获取数据,它会自动处理「下拉重载」、「上拉翻页」的操作,然后把数据更新到 <code>this.data.listPage.goods</code> 下。</p>
<p>View 层只需要描述数据怎么展示:</p>
<pre><code class="html"><view class="good" wx:for="listPage.goods.data">
...
</view>
<view class="no-more" wx:if="listPage.goods.hasMore === false">
没有更多了
</view></code></pre>
<p><code>listpage</code> 还支持多数据列表等其他更多配置,详情看:<a href="https://link.segmentfault.com/?enc=L8CE7q%2BtkREad2wWfcvzfQ%3D%3D.OV3vmy8C2XSebPL51O1CJp3G0MbpF3v9Eo2%2Fj4Wron%2BPHUmZ1LJ7Zc1qaIEc6QST" rel="nofollow">@beautywe/plugin-listpage</a></p>
<h3>缓存策略 Cache</h3>
<p><code>@beautywe/plugin-cache</code> 提供了一个微信小程序端缓存策略,其底层由 <a href="https://link.segmentfault.com/?enc=dIloWuSutqIuVhDm8ysNAQ%3D%3D.%2F%2BWl5%2FfB9N%2FQ0bfrxPmi6Vk7GDbkoqy3uo7UCrr8NgVjYY66Oq21ZfxDLt4SDLpc" rel="nofollow">super-cache</a> 提供支持。</p>
<h5>特性</h5>
<ol>
<li>提供一套「服务端接口耗时慢,但加载性能要求高」场景的解决方案</li>
<li>满足最基本的缓存需求,读取(get)和保存(set)</li>
<li>支持针对缓存进行逻辑代理</li>
<li>灵活可配置的数据存储方式</li>
</ol>
<h5>How it work</h5>
<p>一般的请求数据的形式是,页面加载的时候,从服务端获取数据,然后等待数据返回之后,进行页面渲染:</p>
<p><img src="/img/remote/1460000019709619" alt="" title=""></p>
<p>但这种模式,会受到服务端接口耗时,网络环境等因素影响到加载性能。 </p>
<p>对于加载性能要求高的页面(如首页),一般的 Web 开发我们有很多解决方案(如服务端渲染,服务端缓存,SSR 等)。 <br>但是也有一些环境不能使用这种技术(如微信小程序)。</p>
<p>Super Cache 提供了一个中间数据缓存的解决方案:</p>
<p><img src="/img/remote/1460000019709620" alt="" title=""></p>
<p>思路:</p>
<ol>
<li>当你需要获取一个数据的时候,如果有缓存,先把旧的数据给你。</li>
<li>然后再从服务端获取新的数据,刷新缓存。</li>
<li>如果一开始没有缓存,则请求服务端数据,再把数据返回。</li>
<li>下一次请求缓存,从第一步开始。</li>
</ol>
<p>这种解决方案,舍弃了一点数据的实时性(非第一次请求,只能获取上一次最新数据),大大提高了前端的加载性能。 <br>适合的场景:</p>
<ol>
<li>数据实时性要求不高。</li>
<li>服务端接口耗时长。</li>
</ol>
<h5>使用</h5>
<pre><code class="javascript">import { BtApp } from '@beautywe/core';
import cache from '@beautywe/plugin-cache';
const app = new BtApp();
app.use(cache({
adapters: [{
key: 'name',
data() {
return API.fetch('xxx/name');
}
}]
}));</code></pre>
<p>假设 <code>API.fetch('xxx/name')</code> 是请求服务器接口,返回数据:<code>data_from_server</code></p>
<p>那么:</p>
<pre><code class="javascript">app.cache.get('name').then((value) => {
// value: 'data_from_server'
});</code></pre>
<p>更多的配置,详情看:<a href="https://link.segmentfault.com/?enc=4qGHucZplbVnAjEKPccLfQ%3D%3D.Zj3zmka2FnU6Wz1nrD5Z2obspVzk1AlDg0Ewhf4oAMWxwI2%2FFL6tXNN7opyTp9ax" rel="nofollow">@beautywe/plugin-cache</a></p>
<h3>日志 Logger</h3>
<p>由 <code>@beautywe/logger-plugin</code> 提供的一个轻量的日志处理方案,它支持:</p>
<ol>
<li>可控的 log level</li>
<li>自定义前缀</li>
<li>日志统一处理</li>
</ol>
<h5>使用</h5>
<pre><code class="javascript">import { BtApp } from '@beautywe/core';
import logger from '@beautywe/plugin-logger';
const page = new BtApp();
page.use(logger({
// options
}));</code></pre>
<p><strong>API</strong></p>
<pre><code class="javascript">page.logger.info('this is info');
page.logger.warn('this is warn');
page.logger.error('this is error');
page.logger.debug('this is debug');
// 输出
// [info] this is info
// [warn] this is warn
// [error] this is error
// [debug] this is debug</code></pre>
<p><strong>Level control</strong></p>
<p>可通过配置来控制哪些 level 该打印:</p>
<pre><code class="javascript">page.use(logger({
level: 'warn',
}));</code></pre>
<p>那么 <code>warn</code> 以上的 log (<code>info</code>, <code>debug</code>)就不会被打印,这种满足于开发和生成环境对 log 的不同需求。</p>
<p>level 等级如下:</p>
<pre><code class="javascript">Logger.LEVEL = {
error: 1,
warn: 2,
info: 3,
debug: 4,
};</code></pre>
<p>更多的配置,详情看:<a href="https://link.segmentfault.com/?enc=p9GSQYy%2FWWOF%2FE%2FiemDfcw%3D%3D.bC%2BWsH1ljcv3mlaH1AtOIUCrH0hvAcIxp6TE6a833FxKeJjSJn2G%2Bap3C0gh0K28" rel="nofollow">@beautywe/plugin-logger</a></p>
<h2>BeautyWe Framework</h2>
<p><code>@beautywe/core</code> 和 <code>@beautywe/plugin-...</code> 给小程序提供了:</p>
<ol>
<li>开放原生,支持插件化 —— by core</li>
<li>各种插件 —— by plugins</li>
</ol>
<p>但是,还有很多的开发中实际还会遇到的痛点,是上面两个解决不到的。<br>如项目的组织、规范、工程化、配置、多环境等等</p>
<p>这些就是,「BeautyWe Framework」要解决的范畴。</p>
<p>它作为一套开箱即用的项目框架,提供了这些功能:</p>
<ul>
<li>集成 BeautyWe Core</li>
<li>NPM 支持</li>
<li>全局窗口</li>
<li>全局 Page,Component</li>
<li>全局配置文件</li>
<li>多环境开发</li>
<li>Example Pages</li>
<li>正常项目需要的标配:ES2015+,sass,uglify,watch 等</li>
<li>以及我们认为良好的项目规范(eslint,commit log,目录结构等)</li>
</ul>
<p>也是由于篇幅原因,挑几个有趣的来讲讲,更多的可以看看官方文档:<a href="https://link.segmentfault.com/?enc=KAJ7oR7g5DVjPCAgng6gLg%3D%3D.zrv2xDM0A3PyvZW%2BDtHGZ13Yw016S0VsSbywyLqfaH8%3D" rel="nofollow">BeautyWe</a></p>
<h3>快速创建</h3>
<p>首先安装 <code>@beautywe/cli</code></p>
<pre><code class="shell">$ npm i @beautywe/cli -g</code></pre>
<h5>创建应用</h5>
<pre><code class="shell">$ beautywe new app
> appName: my-app
> version: 0.0.1
> appid: 123456
> 这样可以么:
> {
> "appName": "my-app",
> "version": "0.0.1",
> "appid": "123456"
> }</code></pre>
<p>回答几个问题之后,项目就生成了:</p>
<pre><code class="shell">my-app
├── gulpfile.js
├── package.json
└── src
├── app.js
├── app.json
├── app.scss
├── assets
├── components
├── config
├── examples
├── libs
├── npm
├── pages
└── project.config.json</code></pre>
<h5>创建页面、组件、插件</h5>
<p><strong>页面</strong></p>
<ol>
<li>主包页面:<code>beautywe new page <path|name></code>
</li>
<li>分包页面:<code>beautywe new page --subpkg <subPackageName> <path|name></code>
</li>
</ol>
<p><strong>组件</strong></p>
<ol><li><code>beautywe new component <name></code></li></ol>
<p><strong>插件</strong></p>
<ol><li><code>beautywe new plugin <name></code></li></ol>
<h5>自定义模板</h5>
<p>在 <code>./.templates</code> 目录中,存放着快速创建命令的创建模板:</p>
<pre><code class="shell">$ tree .templates
.templates
├── component
│ ├── index.js
│ ├── index.json
│ ├── index.scss
│ └── index.wxml
├── page
│ ├── index.js
│ ├── index.json
│ ├── index.scss
│ └── index.wxml
└── plugin
└── index.js</code></pre>
<p>可以修改里面的模板,来满足项目级别的自定义模板创建。</p>
<h3>全局窗口</h3>
<p>我们都知道微信小程序是「单窗口」的交互平台,一个页面对应一个窗口。 <br>而在业务开发中,往往会有诸如这种述求:</p>
<ol>
<li>自定义的 toast 样式</li>
<li>页面底部 copyright</li>
<li>全局的 loading 样式</li>
<li>全局的悬浮控件</li>
</ol>
<p>......</p>
<p>稍微不优雅的实现可以是分别做成独立的组件,然后每一个页面都引入进来。 <br>这种做法,我们会有很多的重复代码,并且每次新建页面,都要引入一遍,后期维护也会很繁琐。</p>
<p>而「全局窗口」的概念是:<strong>希望所有页面之上有一块地方,全局性的逻辑和交互,可以往里面搁。</strong></p>
<h5>global-view 组件</h5>
<p>这是一个自定义组件,源码在 <code>/src/components/global-view</code></p>
<p>每个页面的 wxml 只需要在顶层包一层:</p>
<pre><code class="html"><global-view id="global-view">
...
</global-view></code></pre>
<p>需要全局实现的交互、样式、组件,只需要维护这个组件就足够了。</p>
<h3>全局配置文件</h3>
<p>在 <code>src/config/</code> 目录中,可以存放各种全局的配置文件,并且支持以 Node.js 的方式运行。(得益于 <a href="/contents/framework/concept/nodejs-power.md">Node.js Power 特性</a>)。</p>
<p>如 <code>src/config/logger.js</code>:</p>
<pre><code class="javascript">const env = process.env.RUN_ENV || 'dev';
const logger = Object.assign({
prefix: 'BeautyWe',
level: 'debug',
}, {
// 开发环境的配置
dev: {
level: 'debug',
},
// 测试环境的配置
test: {
level: 'info',
},
// 线上环境的配置
prod: {
level: 'warn',
},
}[env] || {});
module.exports.logger = logger;</code></pre>
<p>然后我们可以这样读取到 config 内容:</p>
<pre><code class="javascript">import { logger } from '/config/index';
// logger.level 会根据环境不同而不同。</code></pre>
<p>Beautywe Framework 默认会把 config 集成到 <code>getApp()</code> 的示例中:</p>
<pre><code class="javascript">getApp().config;</code></pre>
<h3>多环境开发</h3>
<p>BeautyWe Framework 支持多环境开发,其中预设了三套策略:</p>
<ul>
<li>dev</li>
<li>test</li>
<li>prod</li>
</ul>
<p>我们可以通过命令来运行这三个构建策略:</p>
<pre><code class="shell">beautywe run dev
beautywe run test
beautywe run prod</code></pre>
<h3>三套环境的差异</h3>
<p>Beautywe Framework 源码默认在两方面使用了多环境:</p>
<ul>
<li>构建任务(<code>gulpfile.js/env/...</code>)</li>
<li>全局配置(<code>src/config/...</code>)</li>
</ul>
<h4>构建任务的差异</h4>
<table>
<thead><tr>
<th>构建任务</th>
<th>说明</th>
<th align="center">dev</th>
<th align="center">test</th>
<th align="center">prod</th>
</tr></thead>
<tbody>
<tr>
<td>clean</td>
<td>清除dist文件</td>
<td align="center">√</td>
<td align="center">√</td>
<td align="center">√</td>
</tr>
<tr>
<td>copy</td>
<td>复制资源文件</td>
<td align="center">√</td>
<td align="center">√</td>
<td align="center">√</td>
</tr>
<tr>
<td>scripts</td>
<td>编译JS文件</td>
<td align="center">√</td>
<td align="center">√</td>
<td align="center">√</td>
</tr>
<tr>
<td>sass</td>
<td>编译scss文件</td>
<td align="center">√</td>
<td align="center">√</td>
<td align="center">√</td>
</tr>
<tr>
<td>npm</td>
<td>编译npm文件</td>
<td align="center">√</td>
<td align="center">√</td>
<td align="center">√</td>
</tr>
<tr>
<td>nodejs-power</td>
<td>编译Node.js文件</td>
<td align="center">√</td>
<td align="center">√</td>
<td align="center">√</td>
</tr>
<tr>
<td>watch</td>
<td>监听文件修改</td>
<td align="center">√</td>
<td align="center"> </td>
<td align="center"> </td>
</tr>
<tr>
<td>scripts-min</td>
<td>压缩JS文件</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">√</td>
</tr>
<tr>
<td>sass-min</td>
<td>压缩scss文件</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">√</td>
</tr>
<tr>
<td>npm-min</td>
<td>压缩npm文件</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">√</td>
</tr>
<tr>
<td>image-min</td>
<td>压缩图片文件</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">√</td>
</tr>
<tr>
<td>clean-example</td>
<td>清除示例页面</td>
<td align="center"> </td>
<td align="center"> </td>
<td align="center">√</td>
</tr>
</tbody>
</table>
<h4>Node.js Power</h4>
<p>Beautywe Framework 的代码有两种运行环境:</p>
<ol>
<li>Node.js 运行环境,如构建任务等。</li>
<li>微信小程序运行环境,如打包到 <code>dist</code> 文件夹的代码。</li>
</ol>
<h5>运行过程</h5>
<blockquote>Node.js Power 本质是一种静态编译的实现。 <br>把某个文件在 Node.js 环境运行的结果,输出到微信小程序运行环境中,以此来满足特定的需求。</blockquote>
<p>Node.js Power 会把项目中 <code>src</code> 目录下类似 <code>xxx.nodepower.js</code> 命名的文件,以 Node.js 来运行, <br>然后把运行的结果,以「字面量对象」的形式写到 <code>dist</code> 目录下对应的同名文件 <code>xxx.nodepower.js</code> 文件去。</p>
<p>以 <code>src/config/index.nodepower.js</code> 为例:</p>
<pre><code class="javascript">const fs = require('fs');
const path = require('path');
const files = fs.readdirSync(path.join(__dirname));
const result = {};
files
.filter(name => name !== 'index.js')
.forEach((name) => {
Object.assign(result, require(path.join(__dirname, `./${name}`)));
});
module.exports = result;</code></pre>
<p>该文件,经过 Node.js Power 构建之后:</p>
<p><code>dist/config/index.nodepower.js</code>:</p>
<pre><code class="javascript">module.exports = {
"appInfo": {
"version": "0.0.1",
"env": "test",
"appid": "wx85fc0d03fb0b224d",
"name": "beautywe-framework-test-app"
},
"logger": {
"prefix": "BeautyWe",
"level": "info"
}
};</code></pre>
<p>这就满足了,随意往 <code>src/config/</code> 目录中扩展配置文件,都能被自动打包。</p>
<p>Node.js Power 已经被集成到多环境开发的 dev, test, prod 中去。</p>
<p>当然,你可以手动运行这个构建任务:</p>
<pre><code class="shell">$ gulp nodejs-power</code></pre>
<h4>NPM</h4>
<p>BeautyWe Framework 实现支持 npm 的原理很简单,总结一句话:</p>
<blockquote>使用 webpack 打包 <code>src/npm/index.js</code> ,以 commonjs 格式输出到 <code>dist/npm/index.js</code>
</blockquote>
<p><img src="/img/remote/1460000019709621" alt="npm-works" title="npm-works"></p>
<p>这样做的好处:</p>
<ol>
<li>实现简单。</li>
<li>让 npm 包能集中管理,每次引入依赖,都好好的想一下,避免泛滥(尤其在多人开发中)。</li>
<li>使用 <code>ll dist/npm/index.js</code> 命令能快速看到项目中的 npm 包使占了多少容量。</li>
</ol>
<h5>新增 npm 依赖</h5>
<p>在 <code>src/npm/index.js</code> 文件中,进行 export:</p>
<pre><code class="javscript">export { default as beautywe } from '@beautywe/core';</code></pre>
<p>然后在其他文件 import:</p>
<pre><code class="javascript">import { beautywe } from './npm/index';</code></pre>
<h2>更多</h2>
<p>总的来说,BeautyWe 是一套微信小程序的开发范式。</p>
<p><code>core</code> 和 <code>plugins</code> 扩展原生,提供复杂逻辑的封装和插拔式使用。</p>
<p>而 <code>framework</code> 则负责提供一整套针对于微信小程序的企业级项目解决方案,开箱即用。</p>
<p>其中还有更多的内容,欢迎浏览官网:<a href="https://link.segmentfault.com/?enc=w6xCs1bPyS%2FqZeL7sr3e8g%3D%3D.5Ujbj8r%2BfVBCCjiUxGVooG9Yr8nchFdr9yKnJvqPIo0%3D" rel="nofollow">beautywejs.com</a></p>
CSS Injection
https://segmentfault.com/a/1190000019425474
2019-06-09T22:13:43+08:00
2019-06-09T22:13:43+08:00
felix
https://segmentfault.com/u/felix_5b3b2d56f1a56
3
<h2>什么是CSS注入</h2>
<p>大家对XSS攻击都非常熟悉了,可能很少关注到CSS注入攻击,以下行为有可能受到CSS注入攻击:</p>
<ul>
<li>从用户提供的URL中引入CSS文件</li>
<li>CSS代码中采用了用户的输入数据</li>
</ul>
<p>可以看下以下两个例子<br><a href="https://link.segmentfault.com/?enc=mfZTalMayuTpYY7SBbLvxQ%3D%3D.rLNwOp0%2BEub%2FxH4BBxBDehri926AA6vIUDR7X0qpKnGM8TkrpLNTNdfk498G2R67jcCvDpNTre%2ForiqWnU8NiA%3D%3D" rel="nofollow">https://www.owasp.org/index.p...</a><br><a href="https://link.segmentfault.com/?enc=TlqQrwyIpqF%2BHl1NZYxZhA%3D%3D.ryT%2BQCQpOHdp1MgGhXgm2aZbltdvNIYHBj8h9RfLpPqGoMFTjNr0Ma8QU3DuaC02xgJ2%2FsyqKP%2BXuHsbMWekyyj0slKbJ9XXpiF5pxCZX6w%3D" rel="nofollow">http://www.thespanner.co.uk/2...</a></p>
<h2>原理</h2>
<p>CSS属性选择器让开发者可以根据属性标签的值匹配子字符串来选择元素。 这些属性值选择器可以做以下操作:<br>1.如果字符串以子字符串开头,则匹配;<br>2.如果字符串以子字符串结尾,则匹配;<br>3.如果字符串在任何地方包含子字符串,则匹配;<br>4.属性选择器能让开发人员查询单个属性的页面HTML标记,并且匹配它们的值。</p>
<p>而在实际环境中,如果一些敏感信息会被存放在HTML标签内,如CSRF token存储在隐藏表单的属性值中,这使得我们可以将CSS选择器与表单中的属性进行匹配,并根据表单是否与起始字符串匹配,加载一个外部资源,例如背景图片,来尝试猜测属性的起始字母。通过这种方式,攻击者可以进行逐字猜解并最终获取到完整的敏感数值。</p>
<h2>防御策略</h2>
<p>想要解决这个问题受害者可以在其服务器实施内容安全策略(CSP),防止攻击者从外部加载CSS代码。</p>
<h2>总结</h2>
<p>作为前端开发者,我们与用户最近,也是网络安全防护的第一道线,要时刻关注来自各方面的网络攻击,保障用户信息的安全。</p>
<h2>参考文献</h2>
<p><a href="https://link.segmentfault.com/?enc=Vd4fBv9MVyqjiKd5FJAlng%3D%3D.P5Ywq1alMHQU%2Bos7LZgS8Ws0cMhKS3l7FQnpqXk6GvNR89ZUTVCkYYFCuvF6twJVsok9aFEkpvfFRMWa6CNHhzDD9ucSH3nXFC3MJ%2FEhhCM%3D" rel="nofollow">https://portswigger.net/kb/is...</a><br><a href="https://link.segmentfault.com/?enc=DvyaXm7UvezgyJ1RR0JiBA%3D%3D.wpJgTzhnfV8wH%2Fqn%2FS4u%2FYV5%2BLxjI36FeEzrdf4mjjJSbNyAC99Mm%2F52zeYFJOiMNcW1k3y6H79Q7iNghpO8sw%3D%3D" rel="nofollow">https://www.freebuf.com/artic...</a><br><a href="https://link.segmentfault.com/?enc=CcrbKF0Qmyv5G6bgCRXyFg%3D%3D.tSts%2FHjAwtZY5MPjfRwKyb8QPAuBHIS8BB3DMbbCA6mLoFEKCWz7QVOdaa1C63lCo6wg5o8iq%2BhvjnS1YX5aYA%3D%3D" rel="nofollow">https://24ways.org/2018/secur...</a></p>
函数式编程(二)
https://segmentfault.com/a/1190000019425365
2019-06-09T21:57:57+08:00
2019-06-09T21:57:57+08:00
felix
https://segmentfault.com/u/felix_5b3b2d56f1a56
2
<h2>高阶函数</h2>
<p>满足以下两点的函数:</p>
<ol>
<li>函数可以作为参数被传递</li>
<li>函数可以作为返回值输出</li>
</ol>
<p>叫高阶函数,很显然js中的函数满足高阶函数的条件。</p>
<p>函数作为参数:</p>
<pre><code class="javascript">function pow(x) {
return x * x;
}
const arr = [1, 2, 3];
const res = arr.map(pow);</code></pre>
<p>函数作为返回值:</p>
<pre><code class="javascript">function getPrintFn() {
function print(msg) {
console.log(msg);
}
return print;
}</code></pre>
<p>高阶函数与函数式编程有什么关系?通过上一篇我们知道函数式编程采用纯函数,那怎么把不纯的函数转化为一个纯函数呢?通过把不纯的操作包装到一个函数中,再返回这个函数(即上面的例子),即可达到目的。</p>
<h2>柯里化(curry)</h2>
<blockquote>只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。</blockquote>
<p>特点:<br>接收单一参数,将更多的参数通过回调函数来搞定; <br>返回一个新函数,用于处理所有的想要传入的参数;<br>需要利用call/apply与arguments对象收集参数;<br>返回的这个函数正是用来处理收集起来的参数;</p>
<pre><code class="javascript">function add(x, y) {
return x + y;
}
// 柯里化
function add(x) {
return function(y) {
return x + y;
}
}
const increment = add(1);
increment(2); // 3</code></pre>
<p>当我们谈论纯函数的时候,我们说它们接受一个输入返回一个输出。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出。curry函数适用于以下场景:</p>
<ul>
<li>延迟执行:不断的柯里化,累积传入的参数,最后执行。</li>
<li>固定易变因素:提前把易变因素,传参固定下来,生成一个更明确的应用函数。最典型的代表应用,是bind函数用以固定this这个易变对象。</li>
</ul>
<h2>代码组合(compose)</h2>
<p>在函数式编程中,通过将一个个功能单一的纯函数组合起来实现一个复杂的功能,就像乐高拼积木一样,这种称为函数组合(代码组合)。下面看一个例子:</p>
<p><img src="/img/bVbtFy3?w=1024&h=442" alt="clipboard.png" title="clipboard.png"></p>
<p>最佳实践是让组合可重用。</p>
<h2>函子</h2>
<p>我们知道,函数式编程实质是通过管道把数据在一系列纯函数间传递,但是,控制流(control flow)、异常处理(error handling)、异步操作(asynchronous actions)和状态(state)呢?还有更棘手的副作用(effects)呢?这些问题的解决就要引入函子的概念了。</p>
<p>我们首先定义一个容器,用来封装数据</p>
<p><img src="/img/bVbtFzD?w=966&h=162" alt="clipboard.png" title="clipboard.png"></p>
<p>函子封装了数据和对数据的操作,functor 是实现了map函数并遵守一些特定规则的容器类型。</p>
<p><img src="/img/bVbtFzF?w=1024&h=247" alt="clipboard.png" title="clipboard.png"></p>
<p>把值装进一个容器,而且只能使用 map 来处理它,这么做的理由到底是什么呢?<br>让容器自己去运用函数能给我们带来什么好处?<br>Functor 是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。当 map 一个函数时,我们让容器自己来运行这个函数,这样容器就可以自由地选择何时何地如何操作这个函数,以致于拥有惰性求值、错误处理、异步调用等等非常牛掰的特性。</p>
<h3>函子的类型</h3>
<p>1.Maybe(处理null问题)<br>2.Either(if…else)<br>3.IO(IO、网络请求、DOM)<br>4.Monad(嵌套问题)</p>
<h4>Maybe</h4>
<p>一种用来处理null和undefined问题的函子,避免繁琐的手动判空操作</p>
<p><img src="/img/bVbtFzS?w=1024&h=362" alt="clipboard.png" title="clipboard.png"></p>
<h4>Either</h4>
<p>一种用来处理if…else问题的函子</p>
<p><img src="/img/bVbtFzV?w=790&h=616" alt="clipboard.png" title="clipboard.png"></p>
<h4>IO</h4>
<p>通过返回一个获取数据的函数来延迟IO的副作用,等调用者去执行有副作用的函数,<br>以保证获取数据过程中的无副作用特性</p>
<p><img src="/img/bVbtFAb?w=1024&h=286" alt="clipboard.png" title="clipboard.png"></p>
<h2>Monad</h2>
<p>monad 是可以变扁(flatten)的实现了of方法的 functor</p>
<p><img src="/img/bVbtFAn?w=1024&h=172" alt="clipboard.png" title="clipboard.png"></p>
<h2>总结</h2>
<p>学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。</p>
<p>参考文档<br><a href="https://link.segmentfault.com/?enc=Jt8F%2BjQ97RNXcaJ6oD%2BbiA%3D%3D.JAJp49FY4JjiptpeCOUtrlYED8j790XCIpw9aM%2BvaRNu2cXkFw1FjC8m%2FZukA1Wder%2FaMUoyxAVcG9qhrRC9clunCUunDc4NW22JWXLClnngXee2UDuy6VZauZ2Gz23d%2B0R7aXxVYSFtNTPOPxT%2BSg%3D%3D" rel="nofollow">https://github.com/xitu/gold-...</a><br><a href="https://link.segmentfault.com/?enc=J5YrQTNvY1UsJNwUeShUMw%3D%3D.w0LRLDlwIVqCKe5hiTpYbnx1uYkmcVAONY%2B6iduaoZsnD%2F0QkVNRF3spF7hGzSHGUCK2bc580iWLm4D9ZYfvr6Xunh0N4wGeP2u7O%2B2rhw0%3D" rel="nofollow">https://llh911001.gitbooks.io...</a></p>
React 高阶组件使用心得
https://segmentfault.com/a/1190000019415744
2019-06-07T17:37:20+08:00
2019-06-07T17:37:20+08:00
csywweb
https://segmentfault.com/u/csywweb
8
<blockquote>高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。<br>具体而言,高阶组件是参数为组件,返回值为新组件的函数。<br><a href="https://link.segmentfault.com/?enc=rs9tE7ayUmMzdGt8Iu1ZlQ%3D%3D.BWze62vxDpDUjdjjUfPiHsGmUaKD0DwfUET5wrAW3iI2dswHYjB7YDm4t7W2K1wgP7xlo2AIT23hLKlZ7BMJDA%3D%3D" rel="nofollow">还不了解高阶组件的点这里</a>
</blockquote>
<p>最近在做项目的时候遇到了一个情形:在系统后台存在多个全局设置开关,例如在多个页面都需要请求<strong>会员设置</strong>的开关。如果在每个需要调用全局设置的地方都去请求一下接口,就会有一种不优雅的感觉,这个时候我就想到利用高阶组件抽象一下。</p>
<h2>版本一</h2>
<pre><code>getMemberSettingHoc = Component => class ContainerComponent extends React.Component {
state = {
loading: true,
memberSetting: ''
}
componentDidMount() {
Api.getSetting().then(memberSetting => {
this.setState({ memberSetting })
})
...doSomething
}
render() {
return (
<Component {...this.props} memberSetting={this.state.memberSetting} />
)
}
}
// 使用高阶组件
class Home extends Component {
...
}
export default getMemberSettingHoc(Home);</code></pre>
<p>这个时候看起来编写一个 <em>getMemberSettingHoc</em> 的高阶组件可以让我们复用获取<strong>会员设置</strong>的逻辑</p>
<h2>版本二</h2>
<p>这个时候如果又多了几个全局开关,例如:交易设置,支付设置,预约设置...</p>
<blockquote>问题来了,这个时候我们还需要编写更多的高阶组件来抽象这些设置吗?</blockquote>
<p>答案是否定的,我们可以实现一个 preMountHoc 来处理这些请求,顾名思义, preMountHoc 就是在挂载之前做事情的高阶组件。</p>
<p>代码如下</p>
<pre><code>const preMountDecorator = doSomething => ContentComponent => class PreMountComponent extends React.Component {
}</code></pre>
<p>我们传入了一个参数,这个参数来传入要请求的接口 <em>doList</em> 和返回的字段 <em>keyList</em> ,在 <em>PreMountComponent</em> 挂载的时候去 doSomething, 然后通过props,传回给传入的 <em>ContentComponent</em>.</p>
<p>我们看下代码</p>
<pre><code>// preMountHoc
const preMountHoc = doSomething => ContentComponent => class PreMountComponent extends React.Component {
constructor() {
super();
this.state = {
loading: true,
result: {},
};
}
componentDidMount() {
const doList = get(doSomething, 'doList', []);
const doListLength = get(doList, 'length') || 0;
const keyList = get(doSomething, 'keyList', []);
const keyListLength = get(keyList, 'length') || 0;
if (doListLength == 0) {
this.setState({ loading: false });
return;
}
Promise.all(doList).then((res) => {
const result = {};
if (doListLength == keyListLength) {
keyList.forEach((el, index) => {
result[el] = res[index];
});
} else {
doList.forEach((el, index) => {
result[`pre_${index}`] = res[index];
});
}
this.setState({ result, loading: false });
});
}
render() {
const { loading, result } = this.state;
if (loading) {
return <BlockLoading loading />;
}
return (
<ContentComponent {...result} {...this.props} />
);
}
};
export default preMountHoc;</code></pre>
<p>给 preMountHoc 传入的需要的请求和返回的字段名,就可以达到我们的目的,来看看使用方法</p>
<pre><code>class ScrmList extends Component {
...
}
export default preMountHoc({
doList: [settingApi.getPower(), settingApi.hasMemberThreshold()],
keyList: ['memberStoreSetting', 'scrmConditionType'],
})(ScrmList)</code></pre>
<p>当然高阶组件也是装饰器,你也可以这么用</p>
<pre><code>@preMountHoc({
doList: [settingApi.getPower(), settingApi.hasMemberThreshold()],
keyList: ['memberStoreSetting', 'scrmConditionType'],
})
class ScrmList extends Component {
...
}
export default ScrmList;</code></pre>
<h2>总结</h2>
<p>高阶组件并不是 React 的特性,是函数式编程的一种范式,最大的好处就是解耦和灵活性,在这分享下最近项目中的思考。</p>
区块链笔记(4)用JS写个简单的区块链原型
https://segmentfault.com/a/1190000019310900
2019-05-28T00:44:22+08:00
2019-05-28T00:44:22+08:00
sept08
https://segmentfault.com/u/sept08
5
<blockquote>介绍了一些关于比特币的概念与机制,为了加深理解,本文基于JavaScript来实现一个简单的区块链原型,后续再对其进行不断丰富。</blockquote>
<h2>1. 概述</h2>
<p>如前所述区块链模型的组成部分,包括区块,区块构成的区块链,以及保存区块链的数据持久层等。一个超简单的UML类图如下:<br><img src="/img/bVbtbqF?w=1906&h=422" alt="图片描述" title="图片描述"><br>由于我是前端的,业余看了这么久区块链的理论,还是手痒痒谢谢代码,把这个类用JavaScript实现一下。写完之后发现目前阶段,对于区块链原型来说还是太过简单,不过如果说用来做前端面试题,考察下面向对象和Promise等知识点倒是挺接洽。</p>
<h2>2. 定义区块数据模型</h2>
<p>摘取比特币区块的详情进行修改,去除所有多余信息,只留下能描述区块最基本的信息,声明区块类如下:</p>
<pre><code>class Block {
constructor(data) {
// 区块的属性值
this.hash = "";
this.height = 0;
this.body = data;
this.time = 0;
this.previousBlockHash = "";
}
}
module.exports.Block = Block;</code></pre>
<h2>3. 数据持久层</h2>
<p>其实用数组实现区块链是最简单的原型方案,但每次重启数组都会被清空,数据并不持久。所以这里引入<code>levelDB</code>数据库作为持久层来保存数据,相关操作可参考<a href="https://link.segmentfault.com/?enc=T8UhrG0S7RRdC2TfmNDUlw%3D%3D.2mPkeyABWrZPuZIqfbb4tQ31GdzLDnPDbYKK49rCMSU%3D" rel="nofollow">level</a>。由于直接调用API,对于应用层来说过于麻烦,所以在此声明一个数据操作类<code>LevelSandbox</code>,该类不像传统的关系型数据库具有增、删、改、查等全部功能,由于区块链上数据的不可更改性,此类只包含增和查的操作。</p>
<h3>3.1 根据key从数据库中获取数据</h3>
<p>本文如下相关异步实现,都采用Promise的方式而非回调,其中好处作为前端工程师此处就不多介绍了,有需要了解的可异步<a href="https://link.segmentfault.com/?enc=SyhcbETSRH%2BDw93UecrzzQ%3D%3D.GC8xzyepKpZBnlxcAjz%2BNZdAUhvhURxWIti6R0DiCdQKf06cmSxJtkxNstHAEELAdgdbG1qy8C7sHd3V5CrStiu7JFpixdJSg9SOlnZ7fw4%3D" rel="nofollow">Promise介绍</a>,自行扩展阅读。</p>
<pre><code>getLevelDBData(key) {
let self = this;
return new Promise(function(resolve, reject) {
self.db.get(key)
.then(value => {
console.log('Value = ' + value);
resolve(value)
})
.catch(err => {
console.log('Not found!');
reject(err)
})
});
}</code></pre>
<h3>3.2 将<code>key/value</code>数据插入数据库中</h3>
<p>以<code>key/value</code>的方式在数据库中存储,其<code>key</code>值得选取,这里考虑使用区块类中声明的<code>height</code>字段,该字段标识一个区块在链中的位序,同时也具有唯一性,非常合适。</p>
<pre><code>addLevelDBData(key, value) {
let self = this;
return new Promise(function(resolve, reject) {
self.db.put(key, value)
.then(() => resolve())
.catch((err) => {
console.log('Block ' + key + ' submission failed');
reject(err)
})
});
}</code></pre>
<h3>3.3 获取数据库中区块总数</h3>
<p><code>createReadStream()</code>方法创建一个读取数据库的流,这里的作用是为了遍历整库以获取存储的区块总数,另外此方法还可通过传参,设置遍历次序,详情可参阅文档。</p>
<pre><code>getBlocksCount() {
let self = this;
return new Promise(function(resolve, reject){
let height = 0;
self.db.createReadStream()
.on('data', function () {
height++;
})
.on('error', function (error) {
reject('Unable to read data stream!', error);
})
.on('close', function () {
resolve(height);
});
});
}</code></pre>
<h2>4. 区块链类</h2>
<p>该类主要负责将新创建的区块添加进区块链,并验证链中各个区块的数据完整性。这个过程中少不了对区块数据的哈希处理,为方便起见,采用第三方库<a href="https://link.segmentfault.com/?enc=bc%2FePHSov5Jo0l5AmqCNfg%3D%3D.aksm4h8NjrD9XmAV6A%2Fb1JZ2ByVqhJO5Mtw166%2FsDfJtgM7wt%2FWUgkvn26zoXprL" rel="nofollow">crypto-js</a>实现的<code>SHA256</code>方法。</p>
<p>构想该类中的主要方法包括:</p>
<ul>
<li>createGenesisBlock():生成起始区块</li>
<li>getBlockHeight():获取区块链长度</li>
<li>getBlock(height):获取指定区块</li>
<li>addBlock(block):将一个新区块加入区块链中</li>
<li>validateBlock(block):验证某个区块</li>
<li>validateChain():验证区块链</li>
</ul>
<p>如下便实现其中主要的几个方法:</p>
<h3>4.1 增加新区块</h3>
<p>各个区块通过<code>previousBlockHash</code>属性,依次指向前一个区块来连接成链的,除首区块该属性为空外。</p>
<pre><code>addBlock(block) {
return this.getBlockHeight()
.then(height => {
区块高度
block.height = height;
// UTC 时间戳
block.time = new Date().getTime().toString().slice(0, -3);
if (height > 0) {
this.getBlock(height - 1)
.then(preBlock => {
// 前一个区块的哈希值
block.previousBlockHash = preBlock.hash;
// 对区块进行哈希处理
block.hash = SHA256(JSON.stringify(block)).toString();
// 将新区快存入库中
this.bd.addLevelDBData(height, JSON.stringify(block));
})
.catch(error => console.log(error));
} else {
block.hash = SHA256(JSON.stringify(block)).toString();
this.bd.addLevelDBData(height, JSON.stringify(block));
}
})
.catch( error => console.log(error));
}</code></pre>
<h3>4.2 验证单个区块完整性</h3>
<p>验证方法就是应用了hash算法的性质:相同的数据经过hash后会生成相同的hash值。</p>
<pre><code>validateBlock(height) {
// 获取区块的值
return this.getBlock(height)
.then(block => {
const objBlock = JSON.parse(block);
let blockHash = objBlock.hash;
objBlock.hash = '';
// 重新生成区块的哈希值
let validBlockHash = SHA256(JSON.stringify(objBlock)).toString();
objBlock.hash = blockHash;
// 比较以验证完整性
if (blockHash === validBlockHash) {
return Promise.resolve({isValidBlock: true, block: objBlock});
} else {
console.log('Block #'+blockHeight+' invalid hash:\n'+blockHash+'<>'+validBlockHash);
return Promise.resolve({isValidBlock: false, block: objBlock});
}
})
}</code></pre>
<h3>4.3 验证整个区块链</h3>
<p>通过依次校验每个区块以验证整条链的完整性。</p>
<pre><code>validateChain() {
let errorLog = [];
let previousHash = '';
this.getBlockHeight()
.then(height => {
for (let i = 0; i < height; i++) {
this.getBlock(i)
.then(block => this.validateBlock(block.height))
.then(({isValidBlock, block}) => {
if (!isValidBlock) errorLog.push(i);
if (block.previousBlockHash !== previousHash) errorLog.push(i);
previousHash = block.hash;
if (i === height - 1) {
if (errorLog.length > 0) {
console.log(`Block errors = ${errorLog.length}`)
console.log(`Blocks: ${errorLog}`)
} else {
console.log('No errors detected')
}
}
})
}
})
}</code></pre>
<h2>5. 生成测试数据</h2>
<pre><code>(function theLoop (i) {
setTimeout(function () {
let blockTest = new Block.Block("Test Block - " + (i + 1));
myBlockChain.addBlock(blockTest).then((result) => {
console.log(result);
i++;
if (i < 10) theLoop(i);
});
}, 10000);
})(0);</code></pre>
<p>作为一个区块链原型的样子算是初见端倪,但就目前的功能来说还非常简陋,说是原型都算抬举了,不过后面慢慢再丰富吧。这里也只算是对之前的一个实践性的小节。</p>
<p>文中以列出主要代码片段,整体实现其实不难,没贴出所有代码主要是为了表述思路更清晰些,若有朋友实现过程中有问题,可文下留言交流。</p>
区块链笔记(3)比特币交易的数据和流程
https://segmentfault.com/a/1190000019291453
2019-05-25T15:06:38+08:00
2019-05-25T15:06:38+08:00
sept08
https://segmentfault.com/u/sept08
5
<blockquote>区块链技术只能用来做关于金融交易的应用么?或许先去了解它有关交易的细节,才能看到是否有其它应用的可能。</blockquote>
<h2>1 交易的数据模型</h2>
<h3>1.1 起因</h3>
<p>在此之前,我们关于<code>Bitcoin Core</code>介绍了许多,以及把它当作工具如何使用,现在我们将进一步来研究下区块链中的数据模型。</p>
<blockquote>为什么说将区块和交易当作<strong>数据模型</strong>来理解非常重要?</blockquote>
<p>我的答案是:为了知道如何使用数据。</p>
<p>我们使用区块链应用与网络中的其它节点进行通信、交互以及协作时,可能更关注的是协议。但如果直接去看协议,可能会不容易看得通透,例如在面对一些问题:通过协议传输的数据长什么样?开发自己的区块链应用时,数据是主角,那如何组织和使用它呢?要搞清楚,数据模型这座大山势必要推倒。</p>
<p>另外谈到数据这个话题,开发者可以通过操作码(<code>Op-code</code>)的方式向区块中嵌入额外的数据,对此目前社区反应出两种不同的声音,以比特币平台为例,一些人认为比特币区块链如此便包含了许多非金融数据,当区块链不断延展的同时,会对那些不在意这些数据的人的存储空间带来了沉重的负担;另一些人则认为这些非金融数据的存在,可能使区块链在金融领域之外,产生更多的应用可能。</p>
<blockquote>
<a href="https://link.segmentfault.com/?enc=P2c%2Fc8tZ8hR0ItQbkJsxpA%3D%3D.afxNBqfelpGUxwgO9zzDAn%2F64jSA3zylxB3v53iX6JBmgeIJLV360YVA2QGcEBFI" rel="nofollow"><strong>Op-code</strong></a>:来自比特币脚本语言的一些操作码,用于在<a href="https://link.segmentfault.com/?enc=o98RuqjgSv8DWq2k1XKhow%3D%3D.4OrMY%2F%2Foem0FhGyaNrNSH2ysn9h1DJc9se%2F3sA4Xjw9pYq7pIkRqEBb%2FzVq9KxBx" rel="nofollow">公钥脚本</a>和<a href="https://link.segmentfault.com/?enc=ys4mlU6DzcU7sQUZLDwgwQ%3D%3D.srvrnAbM2RfBtw2rYNRVLJIZqCjbZeIEo%2FA38p7YDaDvX2NvCqmFvApDzAIixkoewYJAhrzZTt0RfMpeMtUSZg%3D%3D" rel="nofollow">签名脚本</a>中推送数据或执行函数。</blockquote>
<p>其实在社区中看到类似的争论还是蛮有意思的,早期时候,人们为了给比特币交易添加备注信息,或其他和交易本身无关的非金融数据,是通过刻录比特币的方式来进行的,就是在不同的交易中,将output里的验证脚本换成其他数据,这会使得<code>UTXO</code>数据集不断变大,因为这么做会导致这笔交易里的比特币不能再被花费,又因为整个比特币系统出于速度的考虑,会把所有未被花费的交易(<code>UTXO</code>)都存储在内存中,这必然使得网络各节点中包含大量的冗余信息,造成跨节点分类账的维护成本变高。而现在,随着新的改进方案已纳入区块链和操作码中,如<code>Op-return</code>。如此协议已经渐趋成熟,<code>UTXO</code>数据集就不会夸张的膨胀.</p>
<blockquote>
<strong>UTXO</strong>:即未花费的交易输出(Unspent Transaction Outputs),它是比特币交易生成及验证的一个核心概念。交易构成了一组链式结构,所有合法的比特币交易都可以追溯到前向一个或多个交易的输出,这些链条的源头都是挖矿奖励,末尾则是当前未花费的交易输出。另外值得提的一点是,在比特币钱包当中,我们都可以看到账户余额,但在这个账户余额的概念与我们所熟知的银行账户余额有着巨大的不同,其实站在<code>UTXO</code>交易模型上看,并没有什么所谓一个一个的比特币,有的只是<code>UTXO</code>。当我们说张三拥有10个比特币的时候,我们实际上是在说,当前区块链账本中,有若干笔交易的<code>UTXO</code>项的收款人写的是张三的地址,而这些<code>UTXO</code>项的数额总和是10。比特币钱包中所看到的账户余额,实际上则是钱包通过扫描区块链并聚合所有属于该用户的<code>UTXO</code>计算得来的。<p><strong>Op-return</strong>:本质上讲,OP_RETURN是一个脚本操作码,是专门被设计出来承载额外的交易信息的。它的作用就像我们在日常转账过程中的备注信息。通过它发送的数据会和我们进行的比特币交易一样,永久保存在比特币区块链的区块中。</p>
</blockquote>
<h3>1.2 交易的输入和输出</h3>
<p>不论你面对的是哪种区块链应用,交易都是区块链系统中最重要的部分。你可以把交易理解为组成区块链宇宙的原子,正如原子是组成所有生命的基础,交易则是组成数据块的单位。你可能已经注意到了,比特币区块链上所做的任何事情都是,为了确保一笔交易能否被创建,并在网络中传播和验证,以及最终添加到区块链上。当然搞清楚这些具体细节,还是为了以后能够创建自己的区块链应用。所以现在还是一步一步来,先回顾下交易是如何运作的,以及它的输入和输出,这对后面讨论交易的数据模型来说很重要。</p>
<blockquote>
<strong>交易</strong>描述的是一笔资金从它的原始所有者(<code>input</code>)向即将所有者(<code>output</code>)价值转化的数据结构</blockquote>
<p>以下交易详情是使用之前我们介绍过的站点,查看比特币测试链上的一笔交易:<br><img src="/img/bVbsYq4?w=2296&h=786" alt="clipboard.png" title="clipboard.png"><br>从图中显而易见的是,有两笔为0.01BTC的输入,参与了一次0.001BTC的转账后,又退回给原所有者0.019BTC,基于此我想问的是:这些输入从何而来,产生的新输出又去向何处?</p>
<blockquote>一个交易的输入,都来自与另一个交易的未花费输出(<strong>UTXO</strong>)。</blockquote>
<p><img src="/img/bVbsYre?w=1300&h=684" alt="clipboard.png" title="clipboard.png"></p>
<p>在交易发生时有获取账户余额的需求,都是通过统计整个区块链上,该钱包地址关联的所有<strong>UTXO</strong>(未花费交易输出)上的比特币数量来完成的。所以并不存在存储一个账户余额的字段,或者一个比特币的地址。</p>
<h3>1.3 数据模型</h3>
<p>这一小节我们来看交易的信息在数据模型中是如何存储的。如果要求网络返回一个原始交易信息给我们,所得到的可能是像下面这样的信息:</p>
<pre><code>0100000001f3f6a909f8521adb57d898d2985834e632374e770fd9e2b98656f1bf1fdfd427010000006b48304502203a776322ebf8eb8b58cc6ced4f2574f4c73aa664edce0b0022690f2f6f47c521022100b82353305988cb0ebd443089a173ceec93fe4dbfe98d74419ecc84a6a698e31d012103c5c1bc61f60ce3d6223a63cedbece03b12ef9f0068f2f3c4a7e7f06c523c3664ffffffff0260e31600000000001976a914977ae6e32349b99b72196cb62b5ef37329ed81b488ac063d1000000000001976a914f76bc4190f3d8e2315e5c11c59cfc8be9df747e388ac00000000</code></pre>
<p>这是一条还未解码成JSON对象的十六进制数据。虽然确实不是很容易看的懂,但其实组织的还是很有条理的。以上面这条信息为例,从起始位开始,一条交易一般包含如下内容:</p>
<ol>
<li>比特币的版本(Version):<code>01000000</code>
</li>
<li>交易的输入数量(Input Count):<code>01</code>
</li>
<li>交易的输入信息(Input Info):<code>f3f6a909f8521adb57d898d2985834e632374e770fd9e2b98656f1bf1fdfd427010000006b48304502203a776322ebf8eb8b58cc6ced4f2574f4c73aa664edce0b0022690f2f6f47c521022100b82353305988cb0ebd443089a173ceec93fe4dbfe98d74419ecc84a6a698e31d012103c5c1bc61f60ce3d6223a63cedbece03b12ef9f0068f2f3c4a7e7f06c523c3664ffffffff</code>
</li>
<li>交易的输出数量(Output Count):<code>02</code>
</li>
<li>交易的输出信息(Output Info):<code>60e31600000000001976a914977ae6e32349b99b72196cb62b5ef37329ed81b488ac063d1000000000001976a914f76bc4190f3d8e2315e5c11c59cfc8be9df747e388ac</code>
</li>
<li>
<p>锁定时间(loctime):<code>00000000</code>。它表示该条交易最早被确认后,写入的最早区块或最早被确认写入的时间:</p>
<ul>
<li>若该字段非零,且<5亿,则表示该条交易最早被写入的区块的区块号。</li>
<li>若>5亿,则表示该条交易最早被写入区块的时间。</li>
<li>若为零,则表示该条交易立即被写入区块。</li>
</ul>
</li>
</ol>
<p>其中在交易的输入信息和输出信息中,还分别包含了一小段用以验证该次交易是否有效地指令脚本:即输入信息中的解锁脚本(UnLocking script)和输出信息中的锁定脚本(Locking script)。</p>
<blockquote>这里的脚本(script),指的是记录在每条交易中的一系列指令字符,执行用于验证交易是否有效及比特币能否发出。而名称与之类似的比特币脚本语句<br>(Bitcoin Script)是一种基于栈的简单轻量级的语句,被设计用来能通用于一系列硬件平台上做相关运算的指令。我们可以在栈中存储数字或数据常量,并使用一系列前缀为<code>OP_</code>的指令(Opcode)对数据进行操作。例如通过<code>OP_ADD</code>将栈中的两个数据进行相加,通过<code>OP_EQUAL</code>来检查栈顶的两个元素是否相等,<code>OP_DUP</code>复制栈顶的数据等等,总共大概有80多个指令,详见<a href="https://link.segmentfault.com/?enc=2RE7q4SDf4Mzr%2BpAwSfeIg%3D%3D.ZPOUwxs4eqD3fUQDgqY9M1stFH11UMS6RgDFC%2F8N3NsNtcSFDNjFWEmlwx4U7yT%2B" rel="nofollow">Opcodes</a>的维基百科。</blockquote>
<p>接下来我们通过一条简单的算数运算指令来具体观察上面提到的三个概念:解锁脚本、锁定脚本和包含<code>Opcodes</code>指令的比特币脚本语句,算数指令如下:</p>
<pre><code>2 6 OP_ADD 8 OP_EQUAL</code></pre>
<p>比特币脚本语句的执行顺序是从左向右的,并且是基于栈结构的,那么这条语句的执行步骤就应当是:</p>
<ol>
<li>数字2入栈;</li>
<li>数字6入栈;</li>
<li>执行<code>OP_ADD</code>:数字6和2依次出栈后,相加所得的结果(8)再入栈;</li>
<li>数字8入栈</li>
<li>执行<code>OP_EQUAL</code>:数字8和8依次出栈后,进行相等比较,所得的结果(<code>True</code>)再入栈</li>
</ol>
<p>其中我们可以将<code>6 OP_ADD 8 OP_EQUAL</code>这部分视为锁定脚本,它需要满足使其最终结果为<code>True</code>的解锁脚本(2),才能完成算数验证。也就是说如果用这条语句来验证交易的有效性,那么所有知道数字2能满足条件的解锁语句,都可使其生效。</p>
<blockquote>
<p>对于比特币脚本语言有两个特性:</p>
<ul>
<li>无流程控制:语句简单,不存在循环和条件控制,好处是不用担心死循环之类的阻塞性错误;缺点是不够灵活。</li>
<li>无状态:在执行过程前后,不保存任何关于状态的值,好处是安全,不论在哪个平台上执行相同的语句都会得到相同的答案;不足是比较简单。</li>
</ul>
<p>任何实现方式的特点,都有其长短优劣,在做整体方案架构的考量时,应谨慎根据业务场景进行选取。</p>
</blockquote>
<p>而在实际情况中,我们验证交易有效性所使用得解锁脚本(UnLocking script)和锁定脚本(Locking script)构成的比特币脚本语句是如下的结构:</p>
<pre><code><sig> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG</code></pre>
<p>其中对应于解锁脚本(UnLocking script)和锁定脚本(Locking script)的部分分别是:</p>
<ul>
<li>UnLocking script:<code><sig> <pubKey></code>
</li>
<li>Locking script:<code>OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG</code>
</li>
</ul>
<p>忘了说清楚一点,验证交易发生的有效性,并不是用同一个交易的解锁脚本(UnLocking script)和锁定脚本(Locking script)进行验证。而是用当前进行交易的解锁脚本,与该输入回溯的<code>UTXO</code>中的锁定脚本进行验证,而当前交易的锁定脚本则是用来,和未来将要发生的交易中的解锁脚本进行验证。具体验证关系如下图:<br><img src="/img/bVbsYrH?w=1302&h=630" alt="clipboard.png" title="clipboard.png"></p>
<p>交易的有效性验证的工作原理其实很简单,就是利用了非对称加密,在解锁脚本中,包含了钱包所有者用私钥生成的签名。因为只有钱包所有者才有交易权,才能生成判断交易有效地解锁脚本。</p>
<p>具体拆分上面的原始交易数据如下图:<br><img src="/img/bVbsYrR?w=2466&h=1408" alt="clipboard.png" title="clipboard.png"></p>
<p>其中我们将输入信息细化为如下部分:</p>
<ul>
<li>
<code>Previous output hash</code>:所有的输入都可以回溯到一个输出,即上一笔交易所产生的UTXO。</li>
<li>
<code>Previous output index</code>:可能一笔交易会包含多项UTXO,这项便是指定多个UTXO的索引,其中第一个UTXO从0开始算。</li>
<li>
<code>Script Size(bytes)</code>:表示解锁脚本的字节数大小。</li>
<li>
<code>scriptSig</code>:上文谈到的解锁脚本</li>
<li>
<code>Sequence</code>:这目前是比特币废弃的一个属性位,默认设为<code>ffffffff</code>。</li>
</ul>
<p>而输出信息也可细化出如下部分:</p>
<ul>
<li>
<code>Amount</code>:比特币输出的数量,按比特币最小单位(Satoshis)计算,10^8 Satoshis = 1 Bitcoin.</li>
<li>
<code>Script Size(bytes)</code>: 表示锁定脚本的字节数大小。</li>
<li>
<code>scriptPubKey</code>:上文谈到的解锁脚本。</li>
</ul>
<h2>2 创建交易</h2>
<p>通过比特币钱包的GUI工具,虽然能够完成比特币区块链生命周期中的基本操作,但存在一些局限性,所以接下来为了更深入的了解比特币区块链交易的细节,我们将使用调试控制台来创建一个交易,具体步骤如下:</p>
<ol>
<li>在比特币钱包中查看所有的UTXO</li>
<li>查看一个特定UTXO的细节</li>
<li>创建一个原始交易</li>
<li>解码该原始交易</li>
<li>对该原始交易进行签名</li>
<li>将这个交易提交到网络</li>
<li>通过TxID查询所创建的交易</li>
</ol>
<h3>2.1 查看UTXO</h3>
<p>我们可以通过在上一节介绍的比特币钱包的调试窗口(Help-Debug Window)中,查看本钱包所有的UTXO,查询命令为:<code>listunspent</code>。发现查询结果是由一个个UTXO对象构成的数组组成的,截取其中一个UTXO如下所示:</p>
<pre><code>[
...
{
"txid": "811ffa0a5c8020a21f115df020b35a00503e4a87523b025390577ee727fbb73f", // 交易Id
"vout": 1, // 输出序号
"address": "2N1KFMyBJZksopo7gpr7L5QwbtuphLREkGN", // 地址
"redeemScript": "001462fab42642cbfe84c69a9e17fcb6c1ae27f63748", // 赎回脚本
"scriptPubKey": "a9145883d125a1bb6db07e886bb167d966013f407c4487", // 公钥脚本
"amount": 0.01898328, // 可用金额
"confirmations": 26738, // 确认次数
"spendable": true, // 当前钱包是否拥有私钥,以便能够消费该UTXO
"solvable": true, // 是否可用,缺少秘钥时忽略
"safe": true // 未经确认的交易将被认为是不安全的
},
...
]</code></pre>
<h3>2.2 查看一个UTXO详情</h3>
<p>这步我们使用命令:<code>gettxout</code>来查询一个未花费交易的详情,该命令接收三个参数:交易ID、未花费输出的序号(从0开始)、一个可选的布尔值用来控制是否显示内存池中还未验证的输出。<br>复制上一步中的交易ID的查询命令如下:</p>
<pre><code>gettxout 811ffa0a5c8020a21f115df020b35a00503e4a87523b025390577ee727fbb73f 0</code></pre>
<p>运行后得到的结果如下:</p>
<pre><code>{
"bestblock": "00000000000000a88e2e39c56235eb61eaf40fca8273e31d5ce49a4d8577d51f",
"confirmations": 26842, // 验证次数
"value": 0.00100000, // 交易金额(单位是BTC)
"scriptPubKey": { // 解锁脚本
"asm": "OP_HASH160 c6176d6f78b0205a83bf4bbc516a23dc00a4ca64 OP_EQUAL", // 汇编格式(assembly)
"hex": "a914c6176d6f78b0205a83bf4bbc516a23dc00a4ca6487", // 十六进制格式
"reqSigs": 1, // 所需的签名数
"type": "scripthash", // 加密类型
"addresses": [ // 收款地址列表
"2NBJdr34cWkdr31rQRRMvcFYAg7kM8wTiNB"
]
},
"coinbase": false
}</code></pre>
<h3>2.3 创建一个原始交易</h3>
<p>使用命令:<a href="https://link.segmentfault.com/?enc=drg5HJ2h4tmDFgmxCJ7OSQ%3D%3D.F%2FjLJnAoLhZTx3GMyv%2FHFVVALb3kB7t6KIjUosee6fc5sVehFkZJt9kbAHQMA7HcnUgNcoot53zLbyUzTY6W4g%3D%3D" rel="nofollow"><code>createrawtransaction</code></a>,创建一个未签名的序列化交易,该交易并不会存储在钱包或传输到网络。需要两个传参:第一个是前一个输出的引用,第二个是P2PKH或P2SH标准的收款地址及收款数量。创建命令示意如下:</p>
<pre><code>createrawtransaction '[{"txid":"811ffa0a5c8020a21f115df020b35a00503e4a87523b025390577ee727fbb73f","vout": 1}]' '{"2NBn87R8AAwtXUNmmFULvDhmPyeka1X7rRD":0.001, "2NBJdr34cWkdr31rQRRMvcFYAg7kM8wTiNB": 0.001}'</code></pre>
<p>我执行后得到的输出:</p>
<pre><code>02000000013fb7fb27e77e579053023b52874a3e50005ab320f05d111fa220805c0afa1f810100000000ffffffff02a08601000000000017a914cb4a40c6ccaf652cc9a6459047494359c3ff25d787a08601000000000017a914c6176d6f78b0205a83bf4bbc516a23dc00a4ca648700000000</code></pre>
<h3>2.4 解码</h3>
<p>上一步所创建原始交易的输出,是一串十六进制字符串,显然没有什么可读性。为了确认我们所创建的正确性,我们需要将其解码为可读的JSON格式,使用到的命令是<a href="https://link.segmentfault.com/?enc=GqT1OJIrG1ssDjzMUZMufA%3D%3D.sRgZHHx4zKwujMri3mykcb50rrJxcORUYCTBUBgtx7mgZabkCNtZyqVWKa6BQoHZ9Mqy4mFaZgHg%2FbhKqTWQvg%3D%3D" rel="nofollow"><code>decoderawtransaction</code></a>,执行如下:</p>
<pre><code>decoderawtransaction 02000000013fb7fb27e77e579053023b52874a3e50005ab320f05d111fa220805c0afa1f810100000000ffffffff02a08601000000000017a914cb4a40c6ccaf652cc9a6459047494359c3ff25d787a08601000000000017a914c6176d6f78b0205a83bf4bbc516a23dc00a4ca648700000000</code></pre>
<p>输出结果如下:</p>
<pre><code>{
"txid": "8af75c03ca2e7e84135b2809f73e75d758cfc5b72c1e51ae18b770baef844b54",
"hash": "8af75c03ca2e7e84135b2809f73e75d758cfc5b72c1e51ae18b770baef844b54",
"version": 2,
"size": 115,
"vsize": 115,
"weight": 460,
"locktime": 0,
"vin": [
{
"txid": "811ffa0a5c8020a21f115df020b35a00503e4a87523b025390577ee727fbb73f",
"vout": 1,
"scriptSig": {
"asm": "",
"hex": ""
},
"sequence": 4294967295
}
],
"vout": [
{
"value": 0.00100000,
"n": 0,
"scriptPubKey": {
"asm": "OP_HASH160 cb4a40c6ccaf652cc9a6459047494359c3ff25d7 OP_EQUAL",
"hex": "a914cb4a40c6ccaf652cc9a6459047494359c3ff25d787",
"reqSigs": 1,
"type": "scripthash",
"addresses": [
"2NBn87R8AAwtXUNmmFULvDhmPyeka1X7rRD"
]
}
},
{
"value": 0.00100000,
"n": 1,
"scriptPubKey": {
"asm": "OP_HASH160 c6176d6f78b0205a83bf4bbc516a23dc00a4ca64 OP_EQUAL",
"hex": "a914c6176d6f78b0205a83bf4bbc516a23dc00a4ca6487",
"reqSigs": 1,
"type": "scripthash",
"addresses": [
"2NBJdr34cWkdr31rQRRMvcFYAg7kM8wTiNB"
]
}
}
]
}</code></pre>
<h3>2.5 签名</h3>
<p>从上面可读性更好的原始交易信息中,看到交易输入的<code>scriptSig</code>字段为空,这是因为我们还没有为这个签名,证明我们拥有对UTXO的使用权。接下来使用命令<a href="https://link.segmentfault.com/?enc=3jc11N20q218XAkAVGWjnA%3D%3D.bDOiQj5p5tf%2FR67S7N%2BKYy%2BKCKNjEC2svmI8%2BjiIgASFbks7UEYEjUgC8%2FicR94fBBbaxmfk5VCN8zW8ZHq%2BqA%3D%3D" rel="nofollow"><code>signrawtransactionwithwallet</code></a>进行签名:</p>
<pre><code>signrawtransactionwithwallet 02000000013fb7fb27e77e579053023b52874a3e50005ab320f05d111fa220805c0afa1f810100000000ffffffff02a08601000000000017a914cb4a40c6ccaf652cc9a6459047494359c3ff25d787a08601000000000017a914c6176d6f78b0205a83bf4bbc516a23dc00a4ca648700000000</code></pre>
<p>签名成功的输出结果如下:</p>
<pre><code>{
"hex": "020000000001013fb7fb27e77e579053023b52874a3e50005ab320f05d111fa220805c0afa1f81010000001716001462fab42642cbfe84c69a9e17fcb6c1ae27f63748ffffffff02a08601000000000017a914cb4a40c6ccaf652cc9a6459047494359c3ff25d787a08601000000000017a914c6176d6f78b0205a83bf4bbc516a23dc00a4ca64870247304402207fbd59f6e806dc1aab5f602b796dc2ecfa96f0e018c7fe4ecc7dcf190e0619f10220168dffa1d5bd5876518530c72fd3bff59337050949c9732dabcfaeef7533de44012103959e3af1e6ddb01d6ac54966cda59464ab27fcaf34b0dca6df02f75d3df7668800000000",
"complete": true
}</code></pre>
<p>然后对签名后的输出进行JSON解码,会发现输入部分多了些内容:</p>
<pre><code>{
...
"vin": [{
"txid": "811ffa0a5c8020a21f115df020b35a00503e4a87523b025390577ee727fbb73f",
"vout": 1,
"scriptSig": {
"asm": "001462fab42642cbfe84c69a9e17fcb6c1ae27f63748",
"hex": "16001462fab42642cbfe84c69a9e17fcb6c1ae27f63748"
},
"txinwitness": ["304402207fbd59f6e806dc1aab5f602b796dc2ecfa96f0e018c7fe4ecc7dcf190e0619f10220168dffa1d5bd5876518530c72fd3bff59337050949c9732dabcfaeef7533de4401", "03959e3af1e6ddb01d6ac54966cda59464ab27fcaf34b0dca6df02f75d3df76688"],
"sequence": 4294967295
}],
...
}</code></pre>
<h3>2.6 将签名后的交易推送至网络</h3>
<p>使用命令<a href="https://link.segmentfault.com/?enc=HrAjyHbnNSBxh123WlHhOA%3D%3D.DvOLKsm3nMkI%2B6Vd2gBBWWWiJqBEwGzJClyMTSUBvGfErquZHDljFGDY%2Bentv%2FLKT5aJINb4Npy0RSAJuxGF%2Fg%3D%3D" rel="nofollow">sendrawtransaction</a>将已签名的交易推送至网络。</p>
<pre><code>sendrawtransaction 020000000001013fb7fb27e77e579053023b52874a3e50005ab320f05d111fa220805c0afa1f81010000001716001462fab42642cbfe84c69a9e17fcb6c1ae27f63748ffffffff02a08601000000000017a914cb4a40c6ccaf652cc9a6459047494359c3ff25d787a08601000000000017a914c6176d6f78b0205a83bf4bbc516a23dc00a4ca64870247304402207fbd59f6e806dc1aab5f602b796dc2ecfa96f0e018c7fe4ecc7dcf190e0619f10220168dffa1d5bd5876518530c72fd3bff59337050949c9732dabcfaeef7533de44012103959e3af1e6ddb01d6ac54966cda59464ab27fcaf34b0dca6df02f75d3df7668800000000</code></pre>
<p>执行后返回的结果是交易ID的十六进制值:</p>
<pre><code>24cd5619a366ad6a3a34a29766fd5f82c39657bc15dcfdcd4d7363a65f401c8b</code></pre>
<h3>2.7 查看交易详情</h3>
<p>至此整个交易的声明周期就完成了,我们可以通过<a href="https://link.segmentfault.com/?enc=rusJJF8xK0z5hf7omuhUKQ%3D%3D.i7QS4NRvujzcQTNtzEwq7hgzw8COSo0UYnaNQ3phZyJb6aCHthE1lSPwJkWDkDvvAQlxoYPpx932%2BCcjVpEzOQ%3D%3D" rel="nofollow">gettransaction</a>来查看,上一步完成交易的详情:</p>
<pre><code>gettransaction 24cd5619a366ad6a3a34a29766fd5f82c39657bc15dcfdcd4d7363a65f401c8b</code></pre>
<p>得到详情结果如下:</p>
<pre><code>{
"amount": -0.00200000,
"fee": -0.01698328,
"confirmations": 1,
"blockhash": "000000000000006715d295c34b2896d0c28f67a092869610200684e45fdd3ad9",
"blockindex": 1,
"blocktime": 1558762656,
"txid": "24cd5619a366ad6a3a34a29766fd5f82c39657bc15dcfdcd4d7363a65f401c8b",
"walletconflicts": [
],
"time": 1558762586,
"timereceived": 1558762586,
"bip125-replaceable": "no",
"details": [
{
"address": "2NBn87R8AAwtXUNmmFULvDhmPyeka1X7rRD",
"category": "send",
"amount": -0.00100000,
"vout": 0,
"fee": -0.01698328,
"abandoned": false
},
{
"address": "2NBJdr34cWkdr31rQRRMvcFYAg7kM8wTiNB",
"category": "send",
"amount": -0.00100000,
"label": "like you",
"vout": 1,
"fee": -0.01698328,
"abandoned": false
}
],
"hex": "020000000001013fb7fb27e77e579053023b52874a3e50005ab320f05d111fa220805c0afa1f81010000001716001462fab42642cbfe84c69a9e17fcb6c1ae27f63748ffffffff02a08601000000000017a914cb4a40c6ccaf652cc9a6459047494359c3ff25d787a08601000000000017a914c6176d6f78b0205a83bf4bbc516a23dc00a4ca64870247304402207fbd59f6e806dc1aab5f602b796dc2ecfa96f0e018c7fe4ecc7dcf190e0619f10220168dffa1d5bd5876518530c72fd3bff59337050949c9732dabcfaeef7533de44012103959e3af1e6ddb01d6ac54966cda59464ab27fcaf34b0dca6df02f75d3df7668800000000"
}</code></pre>
区块链笔记(2)直观感受比特币
https://segmentfault.com/a/1190000019057798
2019-05-02T18:55:14+08:00
2019-05-02T18:55:14+08:00
sept08
https://segmentfault.com/u/sept08
4
<blockquote>解惑是每个人都在走的一条路,可谁又能解这漫漫无期呢?路上总是麻醉的人多,释怀的人少。</blockquote>
<p>书接上回<a href="https://segmentfault.com/a/1190000018775898">区块链笔记(1)基础概念扫盲</a>,我们讨论了关于比特币以及区块链的许多基础概念,可能通过我略带类比的描述,让你初步有了一些概念,但是对于一个比特币到底长什么样?以及如何使用比特币进行交易?可能还不是很清楚,说的直白点就是:<strong>听过猪叫,但没吃过猪肉</strong>。好吧,那就安排上!</p>
<h2>一、比特币网络</h2>
<p>首先明确两个概念:<strong><code>Bitcoin</code></strong>和<strong><code>Bitcoin Core</code></strong>:</p>
<ul>
<li>
<strong><code>Bitcoin</code></strong>:指比特币用户创建与验证交易的网络。</li>
<li>
<strong><code>Bitcoin Core</code></strong>:指的是帮助你在比特币区块链上构建应用的一套强大的开发者工具。</li>
</ul>
<p>简单说,Bitcoin是我们口口相传的名称、概念,实际使用它还得用Bitcoin Core。</p>
<p>接下来就利用Bitcoin Core来把玩一下比特币,见识一下我们之前说的那些概念究竟实际上长什么样子。</p>
<blockquote>你们是不是以为接下来这篇文章,将是一场低调的炫富:打开我的比特币钱包,不小心让你看到了我的比特币余额,然后演示了一笔交易是如何发生的全过程。我想我或许真的想说那句话:“<strong>我也想低调呀,但是实力不允许呀</strong>”</blockquote>
<p>好了,言归正传,首先要明白比特币三种类型的网络:</p>
<ol>
<li>
<strong>MainNet</strong>:承载着比特币网络上的实时交易,一个比特币值多少钱,说的就是这个网络上的比特币的价值。由于要保证使用的稳定性,那么在该网络上对应用进行构建与测试,显然不是理想的选择。</li>
<li>
<strong>TestNet</strong>:在比特币应用部署到正式环境(MainNet)之前,进行构建与测试用途的环境,详情求查阅<a href="https://link.segmentfault.com/?enc=rh7ppiyIQeuLFWYXSDckeQ%3D%3D.3dB1vnz4mFhWSk%2F7dIETkJChPaJO%2BJyTmoQHgSow8rCZzRkHvtd6JTK%2FoLHyBa6%2F" rel="nofollow">wiki百科</a>。</li>
<li>
<strong>RegNet</strong>:本地验证一些功能性用的。</li>
</ol>
<p>值得说明的是,后两个网络上的比特币其实一毛钱都不值,但是<strong>TestNet</strong>是公网上真实存在的,我们可以在上面观看比特币全流程的生命周期,所以接下来的演示也是基于<strong>TestNet</strong>,下表简单比较了这三类网络的不同。<br><img src="/img/bVbr7Ta?w=1814&h=788" alt="clipboard.png" title="clipboard.png"></p>
<h2>二、Bitcorn Core环境搭建</h2>
<h3>1.下载安装</h3>
<p>移步<a href="https://link.segmentfault.com/?enc=tbIyuVhFAS9kMxAWbe2t0A%3D%3D.COo2kZ%2FfNUB986KttKVqDiGtGOxUAVl%2FYXMx6G8fSBmUhoybnwHh495%2B8X3k1wAx" rel="nofollow">bitcoin.org</a>下载安装适合你电脑版本的,安装步骤比较傻瓜不多说。<br><img src="/img/bVbr7T1?w=2038&h=1444" alt="clipboard.png" title="clipboard.png"></p>
<h3>2. 切换至测试网络</h3>
<p>安装好后,默认打开是正式网络,我们需要通过配置文件将其设置为测试网络。于是找到安装目录,创建配置文件<code>bitcoin.conf</code>,默认安装目录可能会根据操作系统而不同:</p>
<ul>
<li>Mac: ~/Library/Application Support/Bitcoin/</li>
<li>Linux: ~/.bitcoin/</li>
</ul>
<p>打开刚才创建的配置文件<code>bitcoin.conf</code>,写一句话:<code>testnet=1</code>,然后重新打开软件,就会如下图的样子,正在同步测试网络上的数据。<br><img src="/img/bVbr7Ud?w=1468&h=1240" alt="clipboard.png" title="clipboard.png"></p>
<h3>3. 获取测试用的比特币</h3>
<p>有了接入比特币网络的客户端,要进行比特币交易还需要有比特币,比特币不会凭空而来,要么找你认识有比特币的大佬,跪舔他。当然这种方式对于一个有职业操守的开发者来说,实施起来可能比较惆怅。</p>
<p>当然社区也注意到了这一点,所以建了一个比特币的公用池,你可以将你钱包的收款地址留给他,社区会发送少量的比特币供你测试使用,<strong>当你测试完成后,本着职业操守,请将你借出的比特币归还给社区</strong>,虽然这里的比特币并不值什么钱,但是总量也是有限的,要是有人恶意囤积,破坏的是社区的利益。所以还是要注意<strong>职业操守</strong>,<strong>职业操守</strong>,<strong>职业操守</strong>,重要的事情说三遍。</p>
<p>登录比特币测试网络丐帮总舵<a href="https://link.segmentfault.com/?enc=Py1IO7tlY4rbp7MUNmaSrQ%3D%3D.6AHbu6SWM2j12NxvohetSQ7CH62pngMzwif3SDATURcvgxVNd1lS39e%2FP0jjZg9u" rel="nofollow">testnet-faucet</a>,如下图:<br><img src="/img/bVbr7Wd?w=1278&h=1452" alt="clipboard.png" title="clipboard.png"><br>接下来是如何获取,自己钱包的收款地址,打开上面下载安装好的软件,按照下图步骤进行操作,就可生成收款地址,我的地址也附在上面了,欢迎大家给我汇款,体验比特币交易哈。<br><img src="/img/bVbr7WK?w=1916&h=1240" alt="clipboard.png" title="clipboard.png"></p>
<h3>4. 进行交易</h3>
<p>其实很简单,就是两个钱包之间的交易,你可以给我的收款地址汇款比特币,你也可以在本地再建一个钱包,自己和自己交易。方法就是打开比特币的命令行工具(菜单-Help-Debug Window),通过命令在本地创建:</p>
<ul>
<li>
<code>createwallet <walletName></code>:创建一个新的钱包</li>
<li>
<code>loadwallet <walletName></code>:加载已创建的新钱包</li>
</ul>
<p>如下是我的交易记录:</p>
<p><img src="/img/bVbr7Xq?w=1456&h=482" alt="clipboard.png" title="clipboard.png"></p>
<h2>三、查看数据</h2>
<p>上一步我们已经完成了一次完整的交易,具体的交易数据如何查看呢?是不是迫不及待了,我们可以通过一些线上的平台进行具体的查看:</p>
<ul>
<li>
<strong>MainNet</strong>:<a href="https://link.segmentfault.com/?enc=YAOI2I5Uuf6Fn490Jb2hug%3D%3D.6VJEKEQgj13KsoQb31WFnVJQ8EmnUx8SlcTLRwW7g8E%3D" rel="nofollow">https://blockexplorer.com/</a>
</li>
<li>
<strong>TestNet</strong>:<a href="https://link.segmentfault.com/?enc=dorTaDTSPrJ1oz1BjEBwEw%3D%3D.T%2FOG8mClACj3whgKM6cEO42ytTbYo%2BrfJHEX5Tyl4JpPi5E%2BoZGoNvdKRVkKI5G5" rel="nofollow">https://live.blockcypher.com/...</a>
</li>
</ul>
<p>打开网站,我们可以通过在搜索框中输入,交易或区块的地址进行详细的查看:<br><img src="/img/bVbr7XD?w=2386&h=968" alt="clipboard.png" title="clipboard.png"><br>具体每一个字段是什么意思,可以结合上一篇讲到的基础概念进行理解。<br><img src="/img/bVbr7XH?w=2276&h=768" alt="clipboard.png" title="clipboard.png"></p>
<p>最后预告一下,下一篇将对交易的数据模型中的细节进行探讨。如果喜欢欢迎点赞支持。</p>
React Hooks 解析(上):基础
https://segmentfault.com/a/1190000018928587
2019-04-19T23:02:14+08:00
2019-04-19T23:02:14+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
64
<p>欢迎关注我的公众号<code>睿Talk</code>,获取我最新的文章:<br><img src="https://segmentfault.com/img/bVbmYjo" alt="clipboard.png" title="clipboard.png"></p>
<h3>一、前言</h3>
<p>React Hooks 是从 v16.8 引入的又一开创性的新特性。第一次了解这项特性的时候,真的有一种豁然开朗,发现新大陆的感觉。我深深的为 React 团队天马行空的创造力和精益求精的钻研精神所折服。本文除了介绍具体的用法外,还会分析背后的逻辑和使用时候的注意事项,力求做到知其然也知其所以然。</p>
<p>这个系列分上下两篇,这里是下篇的传送门:<br><a href="https://segmentfault.com/a/1190000018950566">React Hooks 解析(下):进阶</a></p>
<h3>二、Hooks 的由来</h3>
<p><code>Hooks</code>的出现是为了解决 React 长久以来存在的一些问题:</p>
<ul><li>带组件状态的逻辑很难重用</li></ul>
<p>为了解决这个问题,需要引入<code>render props</code>或<code>higher-order components</code>这样的设计模式,如<code>react-redux</code>提供的<code>connect</code>方法。这种方案不够直观,而且需要改变组件的层级结构,极端情况下会有多个<code>wrapper</code>嵌套调用的情况。</p>
<p><code>Hooks</code>可以在不改变组件层级关系的前提下,方便的重用带状态的逻辑。</p>
<ul><li>复杂组件难于理解</li></ul>
<p>大量的业务逻辑需要放在<code>componentDidMount</code>和<code>componentDidUpdate</code>等生命周期函数中,而且往往一个生命周期函数中会包含多个不相关的业务逻辑,如日志记录和数据请求会同时放在<code>componentDidMount</code>中。另一方面,相关的业务逻辑也有可能会放在不同的生命周期函数中,如组件挂载的时候订阅事件,卸载的时候取消订阅,就需要同时在<code>componentDidMount</code>和<code>componentWillUnmount</code>中写相关逻辑。</p>
<p><code>Hooks</code>可以封装相关联的业务逻辑,让代码结构更加清晰。</p>
<ul><li>难于理解的 Class 组件</li></ul>
<p>JS 中的<code>this</code>关键字让不少人吃过苦头,它的取值与其它面向对象语言都不一样,是在运行时决定的。为了解决这一痛点,才会有剪头函数的<code>this</code>绑定特性。另外 React 中还有<code>Class Component</code>和<code>Function Component</code>的概念,什么时候应该用什么组件也是一件纠结的事情。代码优化方面,对<code>Class Component</code>进行预编译和压缩会比普通函数困难得多,而且还容易出问题。</p>
<p><code>Hooks</code>可以在不引入 Class 的前提下,使用 React 的各种特性。</p>
<h3>三、什么是 Hooks</h3>
<blockquote>Hooks are functions that let you “hook into” React state and lifecycle features from function components</blockquote>
<p>上面是官方解释。从中可以看出 Hooks 是函数,有多个种类,每个 Hook 都为<code>Function Component</code>提供使用 React 状态和生命周期特性的通道。Hooks 不能在<code>Class Component</code>中使用。</p>
<p>React 提供了一些预定义好的 Hooks 供我们使用,下面我们来详细了解一下。</p>
<h3>四、State Hook</h3>
<p>先来看一个传统的<code>Class Component</code>:</p>
<pre><code class="javascript">class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}</code></pre>
<p>使用 State Hook 来改写会是这个样子:</p>
<pre><code class="javascript">import React, { useState } from 'react';
function Example() {
// 定义一个 State 变量,变量值可以通过 setCount 来改变
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}</code></pre>
<p>可以看到<code>useState</code>的入参只有一个,就是 state 的初始值。这个初始值可以是一个数字、字符串或对象,甚至可以是一个函数。当入参是一个函数的时候,这个函数只会在这个组件初始渲染的时候执行:</p>
<pre><code class="javascript">const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});</code></pre>
<p><code>useState</code>的返回值是一个数组,数组的第一个元素是 state 当前的值,第二个元素是改变 state 的方法。这两个变量的命名不需要遵守什么约定,可以自由发挥。要注意的是如果 state 是一个对象,setState 的时候不会像<code>Class Component</code>的 setState 那样自动合并对象。要达到这种效果,可以这么做:</p>
<pre><code class="javascript">setState(prevState => {
// Object.assign 也可以
return {...prevState, ...updatedValues};
});</code></pre>
<p>从上面的代码可以看出,setState 的参数除了数字、字符串或对象,还可以是函数。当需要根据之前的状态来计算出当前状态值的时候,就需要传入函数了,这跟<code>Class Component</code>的 setState 有点像。</p>
<p>另外一个跟<code>Class Component</code>的 setState 很像的一点是,当新传入的值跟之前的值一样时(使用<code>Object.is</code>比较),不会触发更新。</p>
<h3>五、Effect Hook</h3>
<p>解释这个 Hook 之前先理解下什么是副作用。网络请求、订阅某个模块或者 DOM 操作都是副作用的例子,Effect Hook 是专门用来处理副作用的。正常情况下,在<code>Function Component</code>的函数体中,是不建议写副作用代码的,否则容易出 bug。</p>
<p>下面的<code>Class Component</code>例子中,副作用代码写在了<code>componentDidMount</code>和<code>componentDidUpdate</code>中:</p>
<pre><code class="javascript">class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}</code></pre>
<p>可以看到<code>componentDidMount</code>和<code>componentDidUpdate</code>中的代码是一样的。而使用 Effect Hook 来改写就不会有这个问题:</p>
<pre><code class="javascript">import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}</code></pre>
<p><code>useEffect</code>会在每次 DOM 渲染后执行,不会阻塞页面渲染。它同时具备<code>componentDidMount</code>、<code>componentDidUpdate</code>和<code>componentWillUnmount</code>三个生命周期函数的执行时机。</p>
<p>此外还有一些副作用需要组件卸载的时候做一些额外的清理工作的,例如订阅某个功能:</p>
<pre><code class="javascript">class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}</code></pre>
<p>在<code>componentDidMount</code>订阅后,需要在<code>componentWillUnmount</code>取消订阅。使用 Effect Hook 来改写会是这个样子:</p>
<pre><code class="javascript">import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 返回一个函数来进行额外的清理工作:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}</code></pre>
<p>当<code>useEffect</code>的返回值是一个函数的时候,React 会在下一次执行这个副作用之前执行一遍清理工作,整个组件的生命周期流程可以这么理解:</p>
<blockquote>组件挂载 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 组件卸载</blockquote>
<p>上文提到<code>useEffect</code>会在每次渲染后执行,但有的情况下我们希望只有在 state 或 props 改变的情况下才执行。如果是<code>Class Component</code>,我们会这么做:</p>
<pre><code class="javascript">componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}</code></pre>
<p>使用 Hook 的时候,我们只需要传入第二个参数:</p>
<pre><code class="javascript">useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改变的时候才执行 Effect</code></pre>
<p>第二个参数是一个数组,可以传多个值,一般会将 Effect 用到的所有 props 和 state 都传进去。</p>
<p>当副作用只需要在组件挂载的时候和卸载的时候执行,第二个参数可以传一个空数组<code>[]</code>,实现的效果有点类似<code>componentDidMount</code>和<code>componentWillUnmount</code>的组合。</p>
<h3>六、总结</h3>
<p>本文介绍了在 React 之前版本中存在的一些问题,然后引入 Hooks 的解决方案,并详细介绍了 2 个最重要的 Hooks:<code>useState</code>和<code>useEffect</code>的用法及注意事项。本来想一篇写完所有相关的内容,但发现坑有点深,只能分两次填了:)</p>
Next.js 入门
https://segmentfault.com/a/1190000018888296
2019-04-16T19:14:03+08:00
2019-04-16T19:14:03+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
41
<p>欢迎关注我的公众号<code>睿Talk</code>,获取我最新的文章:<br><img src="https://segmentfault.com/img/bVbmYjo" alt="clipboard.png" title="clipboard.png"></p>
<h3>一、前言</h3>
<p>当使用 React 开发系统的时候,常常需要配置很多繁琐的参数,如 Webpack 配置、Router 配置和服务器配置等。如果需要做 SEO,要考虑的事情就更多了,怎么让服务端渲染和客户端渲染保持一致是一件很麻烦的事情,需要引入很多第三方库。针对这些问题,Next.js提供了一个很好的解决方案,使开发人员可以将精力放在业务上,从繁琐的配置中解放出来。下面我们一起来看看它的一些特性。</p>
<h3>二、特性介绍</h3>
<p>Next.js 具有以下几点特性:</p>
<ul>
<li>默认支持服务端渲染</li>
<li>自动根据页面进行代码分割</li>
<li>简洁的客户端路由方案(基于页面)</li>
<li>基于 Webpack 的开发环境,支持热模块替换</li>
<li>可以跟 Express 或者其它 Node.js 服务器完美集成</li>
<li>支持 Babel 和 Webpack 的配置项定制</li>
</ul>
<h3>三、Hello World</h3>
<p>执行以下命令,开始 Next.js 之旅:</p>
<pre><code>mkdir hello-next
cd hello-next
npm init -y
npm install --save react react-dom next
mkdir pages</code></pre>
<p>在<code>package.json</code>中输入以下内容:</p>
<pre><code>{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}</code></pre>
<p>在 pages 文件夹下,新建一个文件 <code>index.js</code>:</p>
<pre><code class="javascript">const Index = () => (
<div>
<p>Hello Next.js</p>
</div>
)
export default Index</code></pre>
<p>在控制台输入<code>npm run dev</code>,这时候在浏览器输入<code>http://localhost:3000</code>,就能看到效果了。</p>
<p><img src="/img/bVbrgbJ?w=510&h=340" alt="clipboard.png" title="clipboard.png"></p>
<h3>四、路由</h3>
<p>Next.js 没有路由配置文件,路由的规则跟 PHP 有点像。只要在 pages 文件夹下创建的文件,都会默认生成以文件名命名的路由。我们新增一个文件看效果<code>pages/about.js</code></p>
<pre><code class="javascript">export default function About() {
return (
<div>
<p>This is the about page</p>
</div>
)
}</code></pre>
<p>在浏览器输入<code>http://localhost:3000/about</code>,就能看到相应页面了。</p>
<p><img src="/img/bVbrgcp?w=584&h=350" alt="clipboard.png" title="clipboard.png"></p>
<p>如果需要进行页面导航,就要借助<code>next/link</code>组件,将 index.js 改写:</p>
<pre><code class="javascript">import Link from 'next/link'
const Index = () => (
<div>
<Link href="/about">
<a>About Page</a>
</Link>
<p>Hello Next.js</p>
</div>
)
export default Index</code></pre>
<p>这时候就能通过点击链接进行导航了。</p>
<p>如果需要给路由传参数,则使用<code>query string</code>的形式:</p>
<pre><code class="javascript"> <Link href="/post?title=hello">
<a>About Page</a>
</Link></code></pre>
<p>取参数的时候,需要借助框架提供的<code>withRouter</code>方法,参数封装在 query 对象中:</p>
<pre><code class="javascript">import { withRouter } from 'next/router'
const Page = withRouter(props => (
<h1>{props.router.query.title}</h1>
))
export default Page</code></pre>
<p>如果希望浏览器地址栏不显示<code>query string</code>,可以使用<code>as</code>属性:</p>
<pre><code class="javascript"><Link as={`/p/${props.id}`} href={`/post?id=${props.id}`}
<a>{props.title}</a>
</Link></code></pre>
<p>这时候浏览器会显示这样的url:<code>localhost:3000/p/12345</code></p>
<h3>五、SSR</h3>
<p>Next.js 对服务端渲染做了封装,只要遵守一些简单的约定,就能实现 SSR 功能,减少了大量配置服务器的时间。以上面这个 url 为例子,直接在浏览器输入<code>localhost:3000/p/12345</code>是会返回<code>404</code>的,我们需要自己实现服务端路由处理的逻辑。下面以<code>express</code>为例子进行讲解。新建一个 server.js 文件:</p>
<pre><code class="javascript">const express = require('express')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app
.prepare()
.then(() => {
const server = express()
// 处理localhost:3000/p/12345路由的代码
server.get('/p/:id', (req, res) => {
const actualPage = '/post'
const queryParams = { title: req.params.id }
app.render(req, res, actualPage, queryParams)
})
server.get('*', (req, res) => {
return handle(req, res)
})
server.listen(3000, err => {
if (err) throw err
console.log('> Ready on http://localhost:3000')
})
})
.catch(ex => {
console.error(ex.stack)
process.exit(1)
})</code></pre>
<p>当遇到<code>/p/:id</code>这种路由的时候,会调用<code>app.render</code>方法渲染页面,其它的路由则调用<code>app.getRequestHandler</code>方法。</p>
<p>无论是服务端渲染还是客户端渲染,往往都需要发起网络请求获取展示数据。如果要同时考虑 2 种渲染场景,可以用<code>getInitialProps</code>这个方法:</p>
<pre><code class="javascript">import Layout from '../components/MyLayout.js'
import fetch from 'isomorphic-unfetch'
const Post = props => (
<Layout>
<h1>{props.show.name}</h1>
<p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
<img src={props.show.image.medium} />
</Layout>
)
Post.getInitialProps = async function(context) {
const { id } = context.query
const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
const show = await res.json()
console.log(`Fetched show: ${show.name}`)
return { show }
}
export default Post</code></pre>
<p>获取数据后,组件的<code>props</code>就能获取到<code>getInitialProps</code> return 的对象,render 的时候就能直接使用了。<code>getInitialProps</code>是组件的静态方法,无论服务端渲染还是客户端渲染都会调用。如果需要获取 url 带过来的参数,可以从<code>context.query</code>里面取。</p>
<h3>六、CSS in JS</h3>
<p>对于页面样式,Next.js 官方推荐使用 CSS in JS 的方式,并且内置了<code>styled-jsx</code>。用法如下:</p>
<pre><code class="javascript">import Layout from '../components/MyLayout.js'
import Link from 'next/link'
function getPosts() {
return [
{ id: 'hello-nextjs', title: 'Hello Next.js' },
{ id: 'learn-nextjs', title: 'Learn Next.js is awesome' },
{ id: 'deploy-nextjs', title: 'Deploy apps with ZEIT' }
]
}
export default function Blog() {
return (
<Layout>
<h1>My Blog</h1>
<ul>
{getPosts().map(post => (
<li key={post.id}>
<Link as={`/p/${post.id}`} href={`/post?title=${post.title}`}>
<a>{post.title}</a>
</Link>
</li>
))}
</ul>
<style jsx>{`
h1,
a {
font-family: 'Arial';
}
ul {
padding: 0;
}
li {
list-style: none;
margin: 5px 0;
}
a {
text-decoration: none;
color: blue;
}
a:hover {
opacity: 0.6;
}
`}</style>
</Layout>
)
}</code></pre>
<p>注意<code><style jsx></code>后面跟的是模板字符串,而不是直接写样式。</p>
<h3>七、导出为静态页面</h3>
<p>如果网站都是简单的静态页面,不需要进行网络请求,Next.js 可以将整个网站导出为多个静态页面,不需要进行服务端或客户端动态渲染了。为了实现这个功能,需要在根目录新建一个<code>next.config.js</code>配置文件:</p>
<pre><code class="javascript">module.exports = {
exportPathMap: function() {
return {
'/': { page: '/' },
'/about': { page: '/about' },
'/p/hello-nextjs': { page: '/post', query: { title: 'Hello Next.js' } },
'/p/learn-nextjs': { page: '/post', query: { title: 'Learn Next.js is awesome' } },
'/p/deploy-nextjs': { page: '/post', query: { title: 'Deploy apps with Zeit' } }
}
}
}</code></pre>
<p>这个配置文件定义了 5 个需要导出的页面,以及这些页面对应的组件和需要接收的参数。然后在<code>package.json</code>定义下面 2 个命令,然后跑一下:</p>
<pre><code class="javascript">{
"scripts": {
"build": "next build",
"export": "next export"
}
}
npm run build
npm run export</code></pre>
<p>跑完后根目录就会多出一个<code>out</code>文件夹,所有静态页面都在里面。</p>
<p><img src="/img/bVbrpEB?w=179&h=291" alt="clipboard.png" title="clipboard.png"></p>
<h3>八、组件懒加载</h3>
<p>Next.js 默认按照页面路由来分包加载。如果希望对一些特别大的组件做按需加载时,可以使用框架提供的<code>next/dynamic</code>工具函数。</p>
<pre><code class="javascript">import dynamic from 'next/dynamic'
const Highlight = dynamic(import('react-highlight'))
export default class PostPage extends React.Component {
renderMarkdown() {
if (this.props.content) {
return (
<div>
<Highlight innerHTML>{this.props.content}</Highlight>
</div>
)
}
return (<div> no content </div>);
}
render() {
return (
<MyLayout>
<h1>{this.props.title}</h1>
{this.renderMarkdown()}
</MyLayout>
)
}
}
}</code></pre>
<p>当 this.props.content 为空的时候,Highlight 组件不会被加载,加速了页面的展现,从而实现按需加载的效果。</p>
<h3>九、总结</h3>
<p>本文介绍了 Next.js 的一些特性和使用方法。它最大的特点是践行约定大于配置思想,简化了前端开发中一些常用功能的配置工作,包括页面路由、SSR 和组件懒加载等,大大提升了开发效率。</p>
<p>更详细的使用介绍请看<a href="https://link.segmentfault.com/?enc=6rUmhDoDvjuM8NiRwagWGg%3D%3D.gpoSJSZ0nIcMEiG42iBy9pmnP9FlpIzQgrtwRsadvn7e9xCm67%2FrS3BIyGz5aKVU" rel="nofollow">官方文档</a>。</p>
哈希摘要算法
https://segmentfault.com/a/1190000018839324
2019-04-11T22:30:25+08:00
2019-04-11T22:30:25+08:00
petruslaw
https://segmentfault.com/u/petruslaw
4
<h4>前言</h4>
<p>最近在看一些NPM库的时候总是看到各种哈希签名算法,之前工作中也有用到过签名算法,但并没有深入理解过其中的原理,于是找了点资料稍微了解了一下,总结了这篇文章。</p>
<h4>哈希摘要算法</h4>
<p>哈希函数(也称散列函数),是一种根据任意长度数据计算出固定签名长度的算法,比如MD5,SHA系列。</p>
<h4>哈希签名摘要算法特点</h4>
<ul>
<li>不是加密算法,而是一种摘要算法</li>
<li>不可逆,“单向”函数</li>
<li>签名长度固定</li>
<li>存在2的N次方种结果,N表示签名长度</li>
</ul>
<h4>以MD5为例</h4>
<p>MD5是由美国密码学家罗纳德·李维斯特(Ronald Linn Rivest)设计一种加密算法。</p>
<ul>
<li>128个bits长度,也就是16个字节</li>
<li>输出结果由为“0-F”字符组成,不区分大小写</li>
<li>存在2的128次方种输出结果</li>
</ul>
<h4>MD5算法</h4>
<h5>一、源数据处理</h5>
<p>计算原文长度(bits)对512求余的结果,需要填充原文使得原文对512求余的结果等于448, 填充的方法是第一位填充1,其余位填充0。填充完后,信息的长度为512 * N + 448。</p>
<p>剩余64bits存储空间用来填充源信息长度,填充在448byte 数据之后。</p>
<p>最终经过处理后的数据长度为 512 * N。</p>
<p>动手画了一张简单的图来说明:</p>
<p><img src="/img/bVbnLWW?w=621&h=181" alt="clipboard.png" title="clipboard.png"></p>
<h5>二、处理数据</h5>
<p>1、数据进行处理前,会定义4个常量,作为初始值<br>这4个常量分别是</p>
<pre><code class="javascript">var a = 0x67452301;
var b = 0xEFCDAB89;
var c = 0x98BADCFE;
var d = 0x10325476;</code></pre>
<p>翻译成二进制就是</p>
<pre><code class="javascript">var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;</code></pre>
<p>2、将处理后的数据,外循环处理N次,N为第一步中512的整数倍。<br>每次外循环处理的会产生新的“a、b、c、d”值,每次新产生的“a、b、c、d”值会再一次提供给下一次外循环使用</p>
<p>3、在每个外循环中又进行内循环处理64次,在这64次数据处理中会不停的将 512 bytes 数据中的 16个小单元不停的通过4个函数进行交叉处理,共计进行64轮计算。</p>
<p>4、最终生成新的“a、b、c、d”,新的“a、b、c、d”分别是占用32bytes的数据</p>
<p>5、最终生成的“a、b、c、d”转换为对应的ascll占用的字节,32 bytes * 4 = 128 bytes, 一个字节占用8个bytes, 也就是16个字节,16个字节转换为ASCII码,再将ASCII码转换为16进制数据,即可得到一个32个字节长度的hash值。</p>
<p>内外循环代码</p>
<pre><code class="javascript">function binl_md5(x, len) {
/* append padding */
x[len >> 5] = x[len >> 5] | 0x80 << (len % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var i, olda, oldb, oldc, oldd,
a = 1732584193,
b = -271733879,
c = -1732584194,
d = 271733878;
// 每次计算位移值,可以理解为是常量
var ffShift = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22];
var ggShift = [5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20];
var hhShift = [4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23];
var iiShift = [6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21];
// Todo: 四个字节一组,每个组别之间不停的交叉计算,不停的根据已计算出来的值多次计算赋值
// x[i]装的是4个字节的数据
// x.length 为 512 * N / 32
// i += 16 每512bits长度的数据分为了16组,而每次循环的计算单位是以512为一个单元的,所以每次都是+16
for (i = 0; i < x.length; i += 16) {
olda = a;
oldb = b;
oldc = c;
oldd = d;
// 64轮计算中包含原始“a、b、c、d”值。
// 以及位移值,以及一个计算常量,这两个是MD5规范中所定义的常量
a = md5_ff(a, b, c, d, x[i], ffShift[0], -680876936);
d = md5_ff(d, a, b, c, x[i + 1], ffShift[1], -389564586);
c = md5_ff(c, d, a, b, x[i + 2], ffShift[2], 606105819);
b = md5_ff(b, c, d, a, x[i + 3], ffShift[3], -1044525330);
a = md5_ff(a, b, c, d, x[i + 4], ffShift[4], -176418897);
d = md5_ff(d, a, b, c, x[i + 5], ffShift[5], 1200080426);
c = md5_ff(c, d, a, b, x[i + 6], ffShift[6], -1473231341);
b = md5_ff(b, c, d, a, x[i + 7], ffShift[7], -45705983);
a = md5_ff(a, b, c, d, x[i + 8], ffShift[8], 1770035416);
d = md5_ff(d, a, b, c, x[i + 9], ffShift[9], -1958414417);
c = md5_ff(c, d, a, b, x[i + 10], ffShift[10], -42063);
b = md5_ff(b, c, d, a, x[i + 11], ffShift[11], -1990404162);
a = md5_ff(a, b, c, d, x[i + 12], ffShift[12], 1804603682);
d = md5_ff(d, a, b, c, x[i + 13], ffShift[13], -40341101);
c = md5_ff(c, d, a, b, x[i + 14], ffShift[14], -1502002290);
b = md5_ff(b, c, d, a, x[i + 15], ffShift[15], 1236535329);
a = md5_gg(a, b, c, d, x[i + 1], ggShift[0], -165796510);
d = md5_gg(d, a, b, c, x[i + 6], ggShift[1], -1069501632);
c = md5_gg(c, d, a, b, x[i + 11], ggShift[2], 643717713);
b = md5_gg(b, c, d, a, x[i], ggShift[3], -373897302);
a = md5_gg(a, b, c, d, x[i + 5], ggShift[4], -701558691);
d = md5_gg(d, a, b, c, x[i + 10], ggShift[5], 38016083);
c = md5_gg(c, d, a, b, x[i + 15], ggShift[6], -660478335);
b = md5_gg(b, c, d, a, x[i + 4], ggShift[7], -405537848);
a = md5_gg(a, b, c, d, x[i + 9], ggShift[8], 568446438);
d = md5_gg(d, a, b, c, x[i + 14], ggShift[9], -1019803690);
c = md5_gg(c, d, a, b, x[i + 3], ggShift[10], -187363961);
b = md5_gg(b, c, d, a, x[i + 8], ggShift[11], 1163531501);
a = md5_gg(a, b, c, d, x[i + 13], ggShift[12], -1444681467);
d = md5_gg(d, a, b, c, x[i + 2], ggShift[13], -51403784);
c = md5_gg(c, d, a, b, x[i + 7], ggShift[14], 1735328473);
b = md5_gg(b, c, d, a, x[i + 12], ggShift[15], -1926607734);
a = md5_hh(a, b, c, d, x[i + 5], hhShift[0], -378558);
d = md5_hh(d, a, b, c, x[i + 8], hhShift[1], -2022574463);
c = md5_hh(c, d, a, b, x[i + 11], hhShift[2], 1839030562);
b = md5_hh(b, c, d, a, x[i + 14], hhShift[3], -35309556);
a = md5_hh(a, b, c, d, x[i + 1], hhShift[4], -1530992060);
d = md5_hh(d, a, b, c, x[i + 4], hhShift[5], 1272893353);
c = md5_hh(c, d, a, b, x[i + 7], hhShift[6], -155497632);
b = md5_hh(b, c, d, a, x[i + 10], hhShift[7], -1094730640);
a = md5_hh(a, b, c, d, x[i + 13], hhShift[8], 681279174);
d = md5_hh(d, a, b, c, x[i], hhShift[9], -358537222);
c = md5_hh(c, d, a, b, x[i + 3], hhShift[10], -722521979);
b = md5_hh(b, c, d, a, x[i + 6], hhShift[11], 76029189);
a = md5_hh(a, b, c, d, x[i + 9], hhShift[12], -640364487);
d = md5_hh(d, a, b, c, x[i + 12], hhShift[13], -421815835);
c = md5_hh(c, d, a, b, x[i + 15], hhShift[14], 530742520);
b = md5_hh(b, c, d, a, x[i + 2], hhShift[15], -995338651);
a = md5_ii(a, b, c, d, x[i], iiShift[0], -198630844);
d = md5_ii(d, a, b, c, x[i + 7], iiShift[1], 1126891415);
c = md5_ii(c, d, a, b, x[i + 14], iiShift[2], -1416354905);
b = md5_ii(b, c, d, a, x[i + 5], iiShift[3], -57434055);
a = md5_ii(a, b, c, d, x[i + 12], iiShift[4], 1700485571);
d = md5_ii(d, a, b, c, x[i + 3], iiShift[5], -1894986606);
c = md5_ii(c, d, a, b, x[i + 10], iiShift[6], -1051523);
b = md5_ii(b, c, d, a, x[i + 1], iiShift[7], -2054922799);
a = md5_ii(a, b, c, d, x[i + 8], iiShift[8], 1873313359);
d = md5_ii(d, a, b, c, x[i + 15], iiShift[9], -30611744);
c = md5_ii(c, d, a, b, x[i + 6], iiShift[10], -1560198380);
b = md5_ii(b, c, d, a, x[i + 13], iiShift[11], 1309151649);
a = md5_ii(a, b, c, d, x[i + 4], iiShift[12], -145523070);
d = md5_ii(d, a, b, c, x[i + 11], iiShift[13], -1120210379);
c = md5_ii(c, d, a, b, x[i + 2], iiShift[14], 718787259);
b = md5_ii(b, c, d, a, x[i + 9], iiShift[15], -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
// 最终生成4个占用32 bytes控制的值
return [a, b, c, d];
}</code></pre>
<p>四轮计算线性函数</p>
<pre><code class="javascript">F(X,Y,Z) =(X&Y)|((~X)&Z)
G(X,Y,Z) =(X&Z)|(Y&(~Z))
H(X,Y,Z) =X^Y^Z
I(X,Y,Z)=Y^(X|(~Z)) </code></pre>
<p>6、第五点可以解释为什么生成的hash值中只会包含“0-F”,且不区分大小写的原因,长度为16。</p>
<pre><code class="javascript">function rstr2hex(input) {
var hex_tab = '0123456789abcdef',
output = '',
x,
i;
for (i = 0; i < input.length; i += 1) {
x = input.charCodeAt(i);
output += hex_tab.charAt((x >>> 4) & 0x0F) +
hex_tab.charAt(x & 0x0F); x:${input.charCodeAt(i)}, output: ${output}`);
}
return output;
}</code></pre>
<p>以上代码来自 <a href="https://link.segmentfault.com/?enc=T1YWVytFdcqTFAeZ%2B6pa4w%3D%3D.xpycbMt%2BJy9PRV8MFVfqkHcPmPoiIISGHIKQcUVeZ%2FTl0ZINi%2BBEScwILFRYphI0" rel="nofollow">https://github.com/blueimp/JavaScript-MD5</a>,稍有改动。</p>
<h4>适用场景:</h4>
<ul>
<li>私密数据加密,比如用户密码一般都不会明文存储,而是通过加密后存入数据库</li>
<li>赌场开盘前将开票结果公布,开盘后通过签名对比校验是否存在作弊行为</li>
<li>检测文件是否下载完成,比如迅雷下载</li>
<li>...</li>
</ul>
<h4>如何破解</h4>
<p>MD5中,虽然由源文可以推导出签名,反过来,并不能由签名推导出源文。但MD5并不是坚不可摧,目前有两种破解方式</p>
<ul>
<li>碰撞法,虽然MD5签名存在2的128次方种输出结果,但每个签名对应的原文并不是唯一的,只要计算机性能够强大,给予充足的时间,总能找到能输出相同签名的数据源。</li>
<li>映射法,把常规字符串对应的签名存储,比如常用的“123456”,“abcdefg”等。当得到MD5签名时,就可以映射出源数据。</li>
</ul>
<h4>如何防范:</h4>
<ul>
<li>使用安全性更高的SHA256,并不是说SHA256不能被破解,只是相对于MD5来说算法步骤更多,也更复杂,破解难度更大。</li>
<li>源数据 + KEY,比如“123456”加上KEY就变成了“123456@#DFF23DS”,其中“@#DFF23DS”就是服务端存储的KEY。“源数据 + KEY” => 签名。</li>
<li>源数据 + KEY + 动态数据,KEY有可能会被猜到,如果再加上动态数据的话,破解难度会进一步提升,比如用户名、动态密码。“源数据 + KEY + 动态密码” => 签名。</li>
<li>多次MD5,MD5("123456")很容易被猜到,MD5(MD5("123456")),将MD5后的签名再进行一次MD5呢,如果进行三次,十次,是不是破解的难度会更大,当然这么做会增加计算时间,需要权衡。</li>
</ul>
<h5>其他:</h5>
<ul>
<li>中文编码需要转码,否则前端与后端编码后的值可能不一致。</li>
<li>除了MD5算法,还存在很多其他形式的哈希函数算法,比如SHA系列,他们的设计思路大体相同。</li>
</ul>
<h4>参考资料</h4>
<p><a href="https://link.segmentfault.com/?enc=fevOEMFZmHqkyrcvRlV6JA%3D%3D.CYK5DCQ7lldAbqAUkFcBI1wMbh7QIohCzGKaSeL7A9Hlt2XeoOD%2FcV9h3DE777fqIrSBHyAVlaDsCuk2RztMQg%3D%3D" rel="nofollow">阮一峰讲解操作符</a><br><a href="https://link.segmentfault.com/?enc=rrHg3avcHszE5CZ51JTUVA%3D%3D.JFaNtSawuYCX%2FD8FVsYM3cmcGwUYMMvyOJekjsCK3n5Maof5%2FBgDeKsj80tL46hLiRZTF4KoV%2Bx8u2IpKPwgybmqZ950O8dyipB5zS8DIpBi%2BYBqm%2BRndnXuWf8AvRG%2F" rel="nofollow">按位移动操作符</a><br><a href="https://link.segmentfault.com/?enc=Fm0iy16360V%2BbdwWFf3qCQ%3D%3D.MCByx%2B3K8LUDdkFjzKXFu9GDpNM7BbEaDJGqIFBLqO2yrhVulJKmMEWa49vxSVHm" rel="nofollow">各种进制在线转换</a><br><a href="https://link.segmentfault.com/?enc=XKjB9RwaoSCAIAxTEkU7QQ%3D%3D.%2BJEk%2F2CmQBWohmOFdb4WAFznfFoPXvdb%2BmgHAFKT3vJ7RsmxIvg%2BJynSdhjBskRm" rel="nofollow">维基百科MD5</a> <br><a href="https://link.segmentfault.com/?enc=m30hzb8L8vW8kaB%2BtaEsQQ%3D%3D.l%2F9UT2OeVXABZOAnbG%2Bay4O2ERsEp3GnlMgLe%2BnhHbMySuGdpw1LZyBmJZEgOd57" rel="nofollow">维基百科SHA2</a><br><a href="https://link.segmentfault.com/?enc=EWS2%2FKdLZWN9nS19LkYJ2w%3D%3D.nJjwq%2Fayty69Mz5vrENLIDifpu5T5wbdhaqYjbEm4F%2FruvhSdIq2wdLQ47nrk6p1" rel="nofollow">NPM MD5</a></p>
区块链笔记(1)基础概念扫盲
https://segmentfault.com/a/1190000018775898
2019-04-06T13:13:29+08:00
2019-04-06T13:13:29+08:00
sept08
https://segmentfault.com/u/sept08
33
<blockquote>正如民谣像一杯酒,有故事的人听不得。深夜失眠的我,无意翻起中本聪的白皮书,就注定了无眠。今夜的我只醉心于技术,别问是真是假。</blockquote>
<p>这是一篇关于区块链基础的笔记,涉及了我认为对于初学者来说,想要进一步深入前需要了解的最重要的几个概念,概括如下图:<br><img src="/img/bVbpbkc?w=2408&h=828" alt="图片描述" title="图片描述"></p>
<h2>一、金融交易</h2>
<blockquote>在深入了解区块链的技术细节之前,明白它为什么存在对理解它是很有帮助的。</blockquote>
<p>区块链技术,最早是在金融交易领域破土发芽的,但在这之前,金融交易系统已经大体正常运作了许多年。所谓变革的新技术,必定对既有领域中一些核心理念发起了冲击,并提出了自己的解决方案。只在一个行业领域的“兴风作浪”,充其量只能算作改进,若说成是变革,那么这项技术的思想及提供的解决方案,必定能跨越多个行业领域继续“兴风作浪”。</p>
<p>在我们探究金融交易系统的缘起流变之前,先埋下两个问题。</p>
<ul>
<li>金钱在整个金融系统中已无处不在,它到底有什么价值,让人们不得不去使用它?</li>
<li>在当前的金融系统中,有什么我们可以进行改进的方面?</li>
</ul>
<h3>现有的金融系统</h3>
<p>假设我们回到金融交易的历史源头,来为人们设计搭建一套金融系统。首先得明白人们的现状和需求:每个人都拥有属于自己并可以提供给他人的产品或服务,同时也存在自己所稀缺的。为了均衡这种稀缺和富余,每个人可以对自己所拥有东西的价值进行评估,以及所要换取的东西进行预期,这便是贸易的开始。</p>
<p>但以物易物的贸易存在一个不争的事实:交易双方需要通过沟通谈判,来确定交易时物品的价值。当从小范围内的交易,扩展到更大范围的贸易时,相对公允的价格参考就需要被呼唤出来了。而像黄金这样,存量稀缺有价值、属性稳定可长存、体积不大便携带的物品,作为价格参考便再合适不过。更进一步地,人们创造出货币代替了黄金。</p>
<p>由于货币属于人造物,自然就需要对于基于此的交易进行记录和管理,已保证人们拥有财富的安全,于是银行作为可信赖第三方便出现了。人们可在银行中安全的存放金钱,信赖基于货币进行的交易,这是非常有价值的。</p>
<p>同时不免想问,银行又是如何做到这一点的呢?</p>
<blockquote>当一个有价值的创新,成为了日常生活中不可或缺的一部分时,它的实现原理与运行机制,对于大多数使用者来说就会变的透明了。</blockquote>
<p>可想而知,在全球贸易如此普遍的今天,银行维持系统正常运作,所需要的子系统及工具绝对成百上千。这里介绍其中一个重要的工具:<strong>分类账</strong>,它记录的交易信息包括:发送方、接收方、交易时间、交易额度。银行可以利用分类账记录的信息做一些很酷的事情。比如,由于知道谁有钱,谁负债,以及拥有钱的数额,那么便可以确保互不认识的交易双方达成信赖。同时这也帮助解决了所谓的“<strong>双花问题</strong>”,所谓双花问题就是,某人将同一份笔钱花了不止一次。为使一个金融系统正常运行,这种现象是不被允许的,因为你可以想象,这完全是在印钞呀。银行可以通过分类账上的记录,来避免双花问题,因为银行知道第一次交易发生的时间,那么同笔钱第二次交易时就可以认定为无效。</p>
<h3>区块链的改进</h3>
<p>银行作为可信赖第三方,对于金融交易信息的全知视角,便是我们长期以来解决这个问题的一种途径。但是区块链技术提供了一种不同的,或许是更好的方式。因为现有的金融系统,不可否认地依然存在着一些问题:</p>
<h4>交易数据的访问权</h4>
<p>银行在金融系统中的地位太过重要,它确定拥有我们所有的交易数据,但我们不确定银行是否将这些数据同样共享给我们,这样是否真的合理呢?<strong>任何的不公,都是从信息不对称开始的</strong>,你说呢。类似于银行所用的分类账,我们可以创建一个共享的分类账来供所有人访问。</p>
<blockquote>解决一个问题的同时,总会有新的问题伴随着产生。</blockquote>
<p>所创建的共享分类账,是否能达到与目前银行同样的安全与信任程度?</p>
<h4>类似银行的可信赖第三方并不唯一</h4>
<p>现在我们有一个转账需求要完成,可采用的途径并非去银行借记一种方式,还可以信用卡、支付宝、微信、PayPal等等。无论你选择哪种,你都需要给他们提供相关必要的信息。而若当你尝试进行更加复杂的交易时,可能涉及中间环节的公司也会越来越多,这必然会产生额外的费用以及交易延迟。这也是区块链技术尝试解决的一点。</p>
<blockquote>区块链技术发展迅速,想法和工具都会层出不穷,只有我们牢牢抓住目标,我们才能做出明智的取舍。</blockquote>
<h2>二、关于比特币</h2>
<p>区块链现在已经是一个跨越许多平台和行业的热门话题,每天都有许多更新层出不穷,如果我们要去对区块链追根溯源,比特币是一个不容忽视的里程碑。它的相关概念和想法影响着所有后来的其他区块链应用,所以我们可以通过了解比特币,来明白它的核心思想,是如何帮助建立起今天所熟知的区块链的。</p>
<p>比特币使用<strong>块</strong>的概念,来分组和验证交易信息,将若干个交易打包到一个<strong>块</strong>中进行管理。这个想法对比特币乃至区块链来说都非常重要,但却不是比特币首先提出来的,早在1991年,<code>Haber</code>和<code>Stornetta </code>发表了一篇名为<a href="https://link.segmentfault.com/?enc=Ja3AKaqCz7gWLOKd4zqdrA%3D%3D.3W1U1xoD1Q0XTXXjiMT%2Fs9Ar5uUp7Ryju27UadhivNEpqcpHBZ11fyJ0MDn8i4PK" rel="nofollow">How to Time-stamp a Digital Document</a>的论文,提出了一种验证文档的新方式:采用文档更新的时间戳,将不同的版本按此顺序组成一个文档连。如此看这两位老铁也算是区块链的先驱了。</p>
<p>区块链元年2008年,一个叫做中本聪(Satoshi Nakamoto)的神秘作家发表了一封名为<a href="https://link.segmentfault.com/?enc=lSMPyY0Gf1h%2BaxEcLzDeuw%3D%3D.pytRveMc28u48Pa5dW%2B309rAumMlMsFaby7ZgNqxVN0%3D" rel="nofollow">Bitcoin: A Peer to Peer Electronic Cash System</a>的白皮书,奠定了比特币的基础,也完全改变了我们看待和理解货币的方式。接着在2009年1月3日,中本聪发布了比特币软件,同时将第一个比特币带到了这个世界。</p>
<p>中本聪的这篇白皮书真是读一读神清气爽,开篇第一句就开始描述,点对点电子现金系统如何绕过对中心化银行的需求。通篇谈及了交易、时间戳、工作量证明、网络以及许多关于比特币如何运作的概念,或许目前你对于这些概念的技术细节还不是很清楚,不过没关系,当我们初次面对一个新技术的时候都这样。</p>
<p>让我们怀着以下问题,继续往下探索:</p>
<ol>
<li>比特币尝试在解决什么问题?</li>
<li>它提出了怎样的解决方案?</li>
<li>开发这个新系统都用到了什么组件?</li>
</ol>
<h2>三、Hashing</h2>
<p>目前我们对区块链技术的起心动念,已经有了一个大体的了解。接下来我们逐一简介,上图区块链框架中的各部分组件,首先是哈希和一个特殊的哈希函数<strong>SHA256</strong>。</p>
<p>哈希值可以被当做是信息的数字指纹,它是由字母和数字组成的唯一字符串,用以代表或者说是对应一组数据,哈希函数的作用,就是完成给定数据到唯一哈希值得映射。<strong>SHA256</strong>是一个特殊的哈希函数,<strong>SHA</strong>是Secure Hashing Algorithm的缩写,256表示其输出的哈希值是256位的。除此之外还有许多不同的<a href="https://link.segmentfault.com/?enc=VLa0Bhzz0mSozgI1bi1TiA%3D%3D.NWk7jK%2Bz9%2Frny9OatjvgMXIeyDRFGXKrZFDr43Kt6NIprGtcGRHRT4wG7IpU1%2FnJRrzi6ggS22Jl4AyvzJLzpw%3D%3D" rel="nofollow">哈希函数</a>,比特币从中选择了<strong>SHA256</strong>函数,来计算区块链上每个区块的哈希值,这样做的原因是方便对区块的引用,以及完整性的校验。更详细的使用方式可以参考JS类库<a href="https://link.segmentfault.com/?enc=Gq8itaHslB%2Fc5xGdvPz11w%3D%3D.vmAG7mr6tKBZhMDrP%2BUVFBo8M%2Bmgo084XLRuIyDgEBAS%2BuiRvEWJUHGMj%2F2Y3l3j" rel="nofollow">crypto-js</a>。<br><img src="/img/bVbpeN1?w=1286&h=508" alt="图片描述" title="图片描述"><br>为了理解哈希值是如何将数据组成为链的,我们需要对区块和区块链的概念有更多一点的了解。</p>
<h2>四、区块</h2>
<p>如字面意思,区块就是保存区块链上一定量交易信息的容器。如前所述,区块链是一个在网络中存储所有交易记录的共享分类记账单,当我们让它无限地运行下去时,就意味着这个记录所有交易的账单会变得非常庞大。那么将所有的记录作为一个整体来使用或管理,都会非常困难,明智的方法便是化整为零,来存储这些交易信息于许多个小区块中。</p>
<p>那这样包含数量有限交易信息的小区块长什么样呢?一个区块大体分为主体和头部,交易信息存储在主体中,而头部包含了一些额外信息诸如:</p>
<ul>
<li>前一个区块的哈希值。各个区块也就是通过该值相连构成链状结构的。</li>
<li>区块被创建的时间戳。每个区块创建的时间,能够帮助我们确定某项交易生效的时间,这将有效地解决前面讲到的双花问题。</li>
<li>
<code>Merkle root</code>。是一个代表区块中每个交易的哈希值。一个哈希值如何代表区块中所有的交易呢?这里的骚操作是这样的:所有的交易对象两两取哈希值,然后再对得到的哈希值再两两取哈希值,以此类推,所得到的最后一个哈希值即是<code>Merkle root</code>,说白了就是一个二叉树的根节点。这么做的原因是,可以快速查找出区块中不一致的交易,不一致的产生可能是因为传输损坏或篡改。</li>
<li>
<code>Nonce</code>随机数。在创建区块时,网络中可能会存在许多个体同时发起请求,想要创建该区块,这其实就是所谓的“<strong>挖矿</strong>”,那么区块链网络该如何决定由谁来创建一个区块呢?这就是所谓的创建区块的复杂度问题。解决方案的关键就是这个随机数,比特币系统要求每个想要创建下一个区块的请求方,都要提供一个特定的哈希值,这个哈希值由区块所包含的内容<code>blockData</code>和这个随机数<code>nonce</code>,即<code>SHA256({ blockData, nonce})</code>,通过哈希函数计算得到。额外的要求是,所得到的哈希值需要以特定数量<code>0</code>开始,这就需要重复的取哈希值一遍又一遍的计算,直到满足要求。也可以看出,该特定哈希值开头要求得<code>0</code>数量越多,创建该区块的复杂度就越高,反之亦然。</li>
<li>区块大小。顾名思义,就是一个区块能存储信息的大小,这是由开发者在区块链创建时定下来的,当一个区块写入的交易信息达到该区块大小的限制时,就是该创建新区块的时候了。</li>
</ul>
<h2>五、区块链</h2>
<p>区块链是一个共享数字分类账,它包含了发生在网络上的所有交易的历史信息,存储在区块链上的信息永久保存且不可改变。构成区块链的两个重要因素是:区块和哈希值,每一个区块包含自己的哈希值,以及一个指向前一个区块的哈希值,通过哈希值将所有区块按照创建顺序连接成区块链。<br><img src="/img/bVbpqSI?w=1274&h=520" alt="图片描述" title="图片描述"><br>区块链这种由哈希值链接而成的结构,带来了一个有趣的性质:不易更改。当想要更改一个区块的内容时,由于哈希值得完整性,该区块的哈希值也必将更改,又由于该区块的下一个区块的头部中,包含了指向该区块的哈希值,后继区块哈希值的计算包含了指向前序区块的哈希值,前序区块哈希值得更改,就必然连锁的更改所有后继的哈希值。</p>
<blockquote>区块链不易更改的性质,造就了其安全性。</blockquote>
<h2>六、分布式点对点网络</h2>
<p>运行区块链的网络比较特殊,叫做分布式点对点网络。为了能够清楚地理解,就字面可以拆成两块来看:点对点网络和分布式网络。</p>
<p>所谓点对点网络,<em>就是允许网络中的任意两个节点,可以相互直接通信,而不需要通过什么中心化的节点</em>。举些例子,微信,Google的环聊,Skype都属于点对点网络。<em>而分布式网络,允许在许多用户间进行信息传递。</em>这样的定义,我第一次看到也很费解,为了更好地理解,最好的方法论就是<strong>比较与鉴别</strong>。我可以把中心化网络,非中心化网络以及分布式网络拿到一起来看。但在细看之前,需要明白一点,每一种网络都有他们各自的优势和使用场景,我们在区块链中采用分布式网络,只是由于比较来看,分布式网络更适合于区块链应用。<br><img src="/img/bVbptV7?w=1184&h=688" alt="图片描述" title="图片描述"><br>在中心化网络中,所有的信息都集中于一个节点上,其他节点都与中心节点相连。可以拿图书馆的例子来类比,将所有图书都集中保存在唯一的一个图书馆中,人们需要查阅资料或借阅图书,都来这个图书馆就好。好处是书籍与资料集中后便于管理,但问题也是显而易见的:其一,容灾性较差,假如这个唯一的图书馆失火或遭到破坏,由于所有信息只有这里独一份,损失后便无法恢复。其二,对于用户来说,非常不方便,所有人都需要到图书馆才能获取信息,无论你在何处。</p>
<p>于是就有了非中心化网络的改进方案,备份出全部或部分图书馆中的资料,建立多个地区或区域性的图书馆,这样便有了一定的灾备性。而分布式网络,则是把这个思路做到了极致。不需要图书馆了,每个人家里书架摆上50来本树,如果没有的话再相互借。把上述例子中的图书换成交易数据,就是我们比特币网络的样子,每个节点虽然不一定存储了所有的数据,但是通过这个分布式点对点网络,他们可以获取到区块链的所有数据。</p>
<h2>七、内存池</h2>
<p>我们随时的起心动念都可以产生一个交易,但这并不意味着网络处理交易的速度,能够实时的跟上交易产生的速度。也就是说,一定时间内,产生交易的数量可能会超过网络处理交易的数量,那么对于那些暂时未确认写入区块链的交易,就需要一个地方来存储这些信息,这个地方就叫做:内存池。</p>
<p>交易信息被写入区块之前,需要经过网络的确认与验证,这个工作是由区块链网络中一些叫作“矿工”的节点来完成的。具体到如何挖矿稍后介绍,这里先大致有一个概念。<br><img src="/img/bVbpy66?w=1074&h=762" alt="图片描述" title="图片描述"><br><a href="https://link.segmentfault.com/?enc=lJnDByjPzlVBEMu0pMSDFw%3D%3D.xO8MoThOicQ5b7eilkWCKUzU6W%2FFrWFFHzcrXau1kiedSk1E93mhj0tT0wNjUGun" rel="nofollow">blockchain.info</a>这个站点提供了一些比特币区块的专业服务以及加密货币钱包,除此之外,还有一些区块状态的神仙图标可以免费查看。比如我们可以来关注一下,当前<a href="https://link.segmentfault.com/?enc=vU0nIC9NaDx%2FYPTH%2BtP8gQ%3D%3D.evocBHwq3rj%2Fqbp1BRJ9jzlM988yfvl89dO6tngLfUfb2j2g7INAi9MuqCZ47wgT2m25Pmqs%2B35qy9yFi69iXw%3D%3D" rel="nofollow">未确认的区块情况</a>:<br><img src="/img/bVbpzdG?w=2928&h=1784" alt="图片描述" title="图片描述"><br>图中和日期一行的字符串,是一条交易的哈希值;和绿色箭头一行的字符串,是交易双方的钱包,可以类比电子邮箱,只不过这里是用来发送比特币的。</p>
<p>一条交易信息离开内存池的原因,除了由矿工校验过后加入区块,还有一些其他原因:</p>
<ul>
<li>一条交易信息在内存池中停留的过久,若超过14天还没有被矿工写入区块,则会被移除。</li>
<li>在内存池的堆栈中,所有的交易都是按照小费的大小,由高到低排序的。当内存池的存储空间达到上限的时候,此时来了一个小费大于目前内存池中最少小费的交易,那么小费值最少的交易将会被移除内存池。这个小费的额度,是是由发起交易的人确定的,如果希望自己的交易信息被矿工更早的写入区块,可以适当提高小费值。当然并不是不给小费,你的交易信息就不会写入,这要看节点的具体情况。</li>
<li>如果区块中已经有了该交易信息,在写入验证阶段,会将重复的交易信息移除内存池。</li>
<li>如果将要写入的交易信息,和目前区块链中的交易存在冲突,也将会被移除。</li>
</ul>
<h2>八、共识机制</h2>
<p>先来看一个著名的问题:“拜占庭将军问题”。假设有9个拜占庭的将军,各自领着一支军队围着一座城的不同方位,他们之间彼此物理隔离,只能通过传令兵进行通信。他们需要达成共识到底是攻打还是撤退,要么一同攻打要么一同撤退,如果有一支军队和其他军队行动不一致,都会造成失败。同时这其中还有更复杂的因素,或许有某个将军已经叛变了,但其他人还不知道,这就意味着叛变的将军会破坏这次决议的投票;同时负责消息传递的传令兵,在路上也可能发生不可测的状况,而导致送达的信息失真,也可能压根没送到。<br><img src="/img/bVbpzQl?w=922&h=792" alt="图片描述" title="图片描述"><br>我们将将军换成区块链网络中的节点,两个场景中面对的问题是类似的,我们需要一种策略来帮助建立,在通信没那么稳定顺畅的情况下用户之间的信任。这便是所谓的共识机制,达成这个目的有许多备选的算法,比如工作量证明,股份证明等等。</p>
<h3>8.1 工作量证明</h3>
<p>工作量证明最早是比特币提出的一种解决“拜占庭将军问题”的方案,基本思想就是利用,前面谈到的区块头部中那个随机数以及哈希值,计算出这个有着特定数量0开头的哈希值比较困难,也就意味着你在为这件事情付出成本(<em>人们往往会为自己付出成本做的事情负责任,反之如果没有任何成本约束,你说你认真负责,我信你个鬼</em>),同时还需要网络中的其他节点验证起来比较容易,这样可行性才会高。</p>
<p>尝试解题计算这个特定哈希值的节点称作矿工,矿工挖矿解题的过程会消耗计算机的电量,如果你在诸多节点的竞争计算中,获得了下个区块写入区块链的资格,那么你消耗的电量是值得的。同时可想而知,那些没有竞争成功的节点,所消耗的电量真的就是打水漂了。</p>
<p>那为什么有那么多节点前仆后继地要参与到这场算力的竞争中呢?因为获得资格成功写入区块链的节点,会获得比特币网络奖励的比特币,并且这是网络产生新比特币的唯一方式。当然矿工除了从这种方式获得比特币外,他还可以从发生的交易中收取小费。</p>
<p>工作量证明存在的问题:</p>
<ol>
<li>高昂的电费;</li>
<li>矿工对于网络计算资源的垄断,间接的也会造成整个系统的中心化趋势;</li>
</ol>
<p>第一个问题如前所述,对于第二个问题,具体来说,由于解出特定哈希值问题快慢的概率,和几点拥有的计算资源大小成正比。也就是说,你拥有越多的资源你就越容易获得写入下一个区块的资格,也就意味着你获得比特币奖励的概率越大。利益的驱使下,全网的算力资源就会逐渐倾向聚集到少数人身上。</p>
<h3>8.2 股权证明</h3>
<p>股权证明是另一种共识机制算法,这种机制中没有矿工,也不需要投资计算设备用以挖矿获得数字货币,因为从一开始所有的数字货币就已经存在了。取而代之的角色叫做验证者,或者股东,为了验证交易和创建区块,股东需要使用他们的数字货币进行下注,如果他们验证了一个虚假的交易,他们将失去所下注的数字货币,以及未来参与验证的机会,这将会驱策系统只验证正确的交易。</p>
<p>在股权证明机制中,下注越大的股东,获得写入下个区块资格的概率越大。验证者将会按照其下注的比例获得数字货币作为奖励,这就会引出一个问题,对于一个鸡贼的验证者,如果他同时对多个区块进行下注,那么在最后他也并不会损失什么?这无疑是一个潜在的问题,所以网络的策略是对在错误支链上下注的验证着给予处罚,或者对在所有可能的支链上下注的验证者给予处罚。</p>
<p>谁在使用股权证明机制:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=4yr4OBlbh7u6Qc6Edp3K8A%3D%3D.Pf9aY9j8k3izF4km1Rk6hiCEictWZBQ7YN4%2Bzg453BQ%3D" rel="nofollow">以太坊</a></li>
<li>
<a href="https://link.segmentfault.com/?enc=BTRGtdAiBUgSnvob%2B7vYRg%3D%3D.idqYGKcylb5g0MDR7luM88QBfkZi92WMYviCOQRICpc%3D" rel="nofollow">Dash</a>,也是股权证明的先驱之一,它建立在核心比特币平台之上,同时增加了隐私和快速交易的特性。</li>
<li>
<a href="https://link.segmentfault.com/?enc=awRgnN45HUvBC01WuyBtMw%3D%3D.zXO4g7hUqc6toaXQnZfOuNtFLIQxd8qN%2B%2FyzwOstXCo%3D" rel="nofollow">List</a>,旨在允许开发人员创建自己的非中心化应用程序,功能类似于以太坊和NEO,同时允许开发者使用JavaScript。特殊的是,Lisk的共识机制叫做委托的股权证明,网络中只有前101个委托者可以下注,而这些委托者是系统随机选出来的。</li>
</ul>
<h3>8.3 委托式拜占庭容错</h3>
<p>Delegated Byzantine fault tolerance,简称DBFT,是一种基于给不同节点赋予角色来协调共识的一致性算法。</p>
<p>DBFT同样没有矿工角色的节点,取而代之的是将节点分成了<strong>普通节点</strong>和<strong>共识节点</strong>,网络中的绝大多数节点属于普通节点,只能进行转化或交易资产,但是他们不能参与到区块的验证;只有共识节点有权验证写入区块链中的每个区块,它相当于网络中其它节点的代表,类似于我天朝的人民代表制度。普通节点可以转变成共识节点,但不同的平台需要满足的转变标准又太一致。</p>
<p>当决定往区块链写入新区块的时候,会从所有的共识节点中随机选取一个负责写入。在<a href="https://link.segmentfault.com/?enc=i64ABzfvn1fbPEDlXPqy0w%3D%3D.6xKde6XFvbxhgLb7PwonflX7ibjeHQVNpatV2IrrfbA%3D" rel="nofollow">Neo</a>中,这个被选出来写入区块的共识节点叫做<code>speaker</code>(报告人),其余共识节点叫做<code>delegates</code>(代表)。在报告人创建出一个新区块后,并将它提交给诸位代表,如果有2/3的代表证明通过,则这个新区块就被加入到区块链中;如果没有被证明通过,被选择的报告人节点将变成普通的代表,新的报告人节点会被重新选择。</p>
<p>DBFT对比工作量证明更快且消耗的资源更少,同时避免了股权证明可能会出现的区块链分叉现象,这并不是说DBFT就完美无缺了:</p>
<ol>
<li>我们假定有一个不诚实的报告人节点存在(这是可能发生的,因为报告人节点是从所有共识节点中随机选取的),如果发生了,那么网络就需要依赖诚实的代表节点数量能过超过2/3。</li>
<li>很难避免代表节点存在行为上的欺诈,系统会将代表的历史行为数据,公开给所有选民进行查看。</li>
</ol>
<blockquote>总的来说,目前还没有完美的共识机制,目测以后也不可能会有,因为这需要根据具体的需求和使用场景进行权衡。同时了解新出现的区块链应用,所提出自己的共识机制,并分析其优缺点,是很开眼界且有趣的。</blockquote>
前端权限控制
https://segmentfault.com/a/1190000018759018
2019-04-03T22:45:11+08:00
2019-04-03T22:45:11+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
24
<p>欢迎关注我的公众号<code>睿Talk</code>,获取我最新的文章:<br><img src="https://segmentfault.com/img/bVbmYjo" alt="clipboard.png" title="clipboard.png"></p>
<h3>一、前言</h3>
<p>在成熟的电商系统中,权限管理是不可或缺的一个环节。灵活的权限管理有助于管理员对不同的人员分配不同的权限,在满足业务需要的同时保证敏感数据只对有权限的人开放。笔者最近对系统的权限管理做了一次改造,在此分享一些经验以供参考。</p>
<h3>二、权限管理基础</h3>
<p>权限管理一般分以下 3 个基础概念:</p>
<ul>
<li>功能点</li>
<li>角色</li>
<li>用户</li>
</ul>
<p>它们之间的关系一句话就能说清楚:一个用户可以拥有多个角色,而一个角色可以包含多个功能。比如一个员工可以既有收银员的角色,也有库管员的角色。对于收银员这个角色,可以有开单收银、查看订单、查看会员信息等功能点。</p>
<p><img src="/img/bVbqRNA?w=382&h=226" alt="clipboard.png" title="clipboard.png"></p>
<p>此外还有 2 个概念:</p>
<ul>
<li>功能权限</li>
<li>数据权限</li>
</ul>
<p>它们之间的关系举例来说明:<br>想象一个连锁店的场景,某个门店的管理员具有查看营收的功能权限,和查看自己门店数据的数据权限;高级管理员同样拥有查看营收的功能权限,并且有更高的数据权限,可以查看所有门店数据的数据。</p>
<h3>三、前端权限控制</h3>
<p>下面我们聚焦到前端领域,聊聊前端应该怎么做权限设计。前端本质上只有 1 种权限类型:组件权限。为了更好的理解和管理,又将组件权限拆分为以下 3 类:</p>
<p><img src="/img/bVbqRR6?w=435&h=222" alt="clipboard.png" title="clipboard.png"></p>
<p>每一个权限最终都会落到权限点上。权限点可以理解为一个数据编码,有这个权限点就说明有对应的功能权限。权限点的编码要注意 2 点:</p>
<ul>
<li>全局唯一</li>
<li>尽量短小(减少带宽消耗,因为一个用户可能会有很多权限点)</li>
</ul>
<p>需要控制权限的地方,都要定义一个权限点,然后告诉后端。一个用户所有的权限点会以数组的形式返回。判断是否有权限就是从数组中匹配一个元素。下面以 React 为例,聊聊具体的实现方式。</p>
<p>对于页面的权限判断,可以在 React Router 的 onEnter 回调中判断:</p>
<pre><code class="javascript">// 编码映射,下面的 getUrlCodeByName 会用到
export default {
order_list: 'zaq0', // 订单列表
order_detail: 'xsw1', // 订单详情
order_refund_list: 'cde2', // 订单退款列表
order_refund_detail: 'vfr3', // 订单退款详情
order_deduct_modify: 'bgt4', // 订单修改业绩
};
function canAccessUrl(urlName) {
const moduleCode = getUrlCodeByName(urlName); // 权限点一般是一个没意义的编码,为了易于理解,前端做了一个编码映射
return accesses.u.indexOf(moduleCode) > -1; // accesses.u 数组是后端返回的所有 url 权限点
}
function routerOnEnterCheck(urlName) {
return function routerOnEnter(nextState, replace) {
if (!canAccessUrl(urlName)) {
replace('/unauthorized');
}
};
}
...
{
path: 'list',
getComponent: loadAsync(() => import(/* webpackChunkName: "order" */ '../../order/List')),
onEnter: routerOnEnterCheck('order_list'),
}
...</code></pre>
<p>对于菜单和组件的权限判断,大体上长这样:</p>
<pre><code class="javascript">// 用以缓存是否有权限访问组件
const componentAccessCache = {};
/**
* 检查访问组件的权限
* 一个组件可能会重复 render 多次,而组件权限的数量可能会超多(上百个),因此将权限缓存起来以提高性能
*/
function canAccessComponent(module, componentName) {
if (!module || !componentName) {
console.error(`canAccessComponent ${module} ${componentName} 缺参数`);
}
const key = `${module}.${componentName}`;
let result = componentAccessCache[key];
if (result !== undefined) {
return result;
}
const moduleCode = getComponentCodeByModuleAndName(module, componentName); // 权限点一般是一个没意义的编码,为了更易于理解,前端做了一个编码映射
result = accesses.c.indexOf(moduleCode) > -1; // accesses.c 数组是后端返回的所有组件权限点
componentAccessCache[key] = result;
return result;
}
class SomeComponent extends PureComponent {
...
render() {
return (
...
{
canAccessComponent('asset', 'buy_pay') &&
<Button
type="primary"
className="btn-buy"
onClick={() => (buy(num))}
>
立即订购
</Button>
}
...
)
}
}</code></pre>
<p>组件权限点的判断也可以封装成高阶组件的形式,这样看起来会舒服一点,但本质上是一样的,不再赘述。</p>
<p>权限点的获取笔者放在了 node 端,通过全局变量的形式注入到页面中,保证首屏的时候呈现的页面是由权限点过滤过的。此外接口返回的权限点是一个一维数组,为了加快前端检索速度,在 node 端根据编码规则将权限点分为 3 类(菜单/页面/组件),具体细节就不细说了。</p>
<h3>四、总结</h3>
<p>本文介绍了权限管理的基础知识,还结合 React 讲解了前端权限控制的一些细节。技术方案比较简单,真正麻烦的是每一个权限点的定义及录入,以及对现有系统的改造。改造过程中可以分模块进行迭代,毕竟罗马不是一天就能建成。</p>
React中禁止页面滚动
https://segmentfault.com/a/1190000018598630
2019-03-21T11:42:20+08:00
2019-03-21T11:42:20+08:00
sept08
https://segmentfault.com/u/sept08
6
<p>最近用react做了一个H5端的页面,主要实现了一个弹层滑动选择的功能,效果如图:<br><img src="/img/bVbqcbu?w=748&h=1334" alt="图片描述" title="图片描述"><br>遇到了一个问题,当在底部弹出层进行滚动选择城市区划时,蒙版后的页面也会随着滚动。</p>
<p>这种现象在开发过程中经常会遇到,常规思路就是使用<code>event.preventDefault</code>阻止父级元素的滚动:</p>
<pre><code><div className="picker-column">
<div
className="picker-scroller"
style={style}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
onTouchCancel={this.handleTouchCancel}
>
{this.renderItems()}
</div>
</div></code></pre>
<p>滚动事件代码片段</p>
<pre><code>handleTouchMove = (event) => {
event.preventDefault();
...
};</code></pre>
<p>但这波操作过后,却未能如愿以偿,在调试的时候Chrome的告警,如冷冷的冰雨打在我的脸上:<br><img src="/img/bVbqchu?w=1272&h=554" alt="图片描述" title="图片描述"><br>根据告警关键字用Google百度了一番,等到了如下结论:</p>
<blockquote>由于浏览器必须要在执行事件处理函数之后,才能知道有没有调用 <code>preventDefault()</code> ,这就导致了浏览器不能及时响应滚动,略有延迟。<p>所以为了让页面滚动的效果如丝般顺滑,从 chrome56 开始,在 window、document 和 body 上注册的 <code>touchstart</code> 和 <code>touchmove</code> 事件处理函数,会默认为设置<code>passive: true</code>。浏览器忽略 preventDefault() 就可以第一时间滚动了。</p>
</blockquote>
<p><img src="/img/bVbqcjL?w=1526&h=1048" alt="图片描述" title="图片描述"><br>细细揣测一番,其实官方的考虑还是有道理的,也是周到的。在CSS中提供了一个属性<a href="https://link.segmentfault.com/?enc=lq%2FQrBZ8vArAoUCJBWsSNg%3D%3D.1ubiqvCL2locX2MYSBU9jrYmkj5UsRx5iO0j3jpU8Z6JkkHpClhbFg9y7kv%2Fg1GfUjR1XUW4K4b3QpubMRc7s0uo3VPNSml0quJkyPTUyNU%3D" rel="nofollow"><code>touch-action</code></a>,用于指定某个给定的区域是否允许用户操作,以及如何响应用户操作。<br><img src="/img/bVbqcnq?w=2658&h=1444" alt="图片描述" title="图片描述"><br>据此,我的解决方案就是设置这个CSS属性:</p>
<pre><code>touch-action: none;</code></pre>
<p>感觉总算万事大吉利了,那个手机试一把,用iPhone的Safari浏览器代开后,依然并没有什么卵用。是的,九成是浏览器兼容问题,查看<a href="https://link.segmentfault.com/?enc=LoZQC%2BwLx0IUZFvcxUHcjA%3D%3D.xkfT4rY9T9pS1xPUA5YaldRcbG6M%2BXo6MMx9O0z6I%2BpfbAhYaheW6kPFSc%2FpWwfG" rel="nofollow">CanIUse</a>,果不其然。<br><img src="/img/bVbqcpJ?w=2528&h=1324" alt="图片描述" title="图片描述"><br>那么既然如此,剩下的解决方案,就只有在绑定事件的时候显式的设置<code>{ passive: false }</code>,查了一圈React文档也没发现,可以支持配置这个属性的方法。此处真心感叹一句不如Vue方便,如果是Vue就可以这么写:</p>
<pre><code><div v-on:touchmove.prevent="handleTouchMove"></div></code></pre>
<p>既然如此,就只能用原生的事件绑定了</p>
<pre><code>document.getElementById("picker").addEventListener('touchmove', this.handleTouchMove, { passive: false });</code></pre>
<p>终于,世界和平了。</p>
函数式编程(一)
https://segmentfault.com/a/1190000018519264
2019-03-15T14:59:30+08:00
2019-03-15T14:59:30+08:00
felix
https://segmentfault.com/u/felix_5b3b2d56f1a56
0
<h2>什么是函数式编程</h2>
<p>函数式编程是一种编程范式,常见的编程范式有以下三种:</p>
<ul>
<li>命令式编程</li>
<li>声明式编程</li>
<li>函数式编程</li>
</ul>
<p>函数式编程的本质是将计算描述为一种表达式求值。在函数式编程中,函数作为一等公民,可以在任何地方定义(在函数内或函数外),可以作为函数的参数和返回值,可以对函数进行组合。</p>
<p>函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你。看个简单的例子:</p>
<pre><code> // 非函数式的例子
let count = 0;
function increment() {
count++; // 依赖于函数外部的值,并改变了它的值
}
// 函数式的例子
function increment(count) {
return count++;
}</code></pre>
<h2>为什么采用函数式编程</h2>
<p>函数式编程不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这些写代码容易进行推理,不易犯错,而且单测和调试都更简单。即函数编程采用纯函数。</p>
<blockquote>纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。</blockquote>
<p>副作用可能包含,但不限于:</p>
<ul>
<li>更改文件系统</li>
<li>往数据库插入记录</li>
<li>发送一个 http 请求</li>
<li>可变数据</li>
<li>打印/log</li>
<li>获取用户输入</li>
<li>DOM 查询</li>
<li>访问系统状态</li>
</ul>
<blockquote>副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。</blockquote>
<p>纯函数的好处:</p>
<p>纯函数能根据输入来做缓存(memoize技术)</p>
<pre><code> const memoize = function(f) {
const cache = {};
return function() {
const argStr = JSON.stringify(arguments);
if (!cache[argStr]) {
cache[argStr] = f.apply(f, arguments);
}
return cache[argStr];
}
}</code></pre>
<p>可移植性/自文档化<br>纯函数的输出只依赖与它的输入,依赖很明确,易于理解。由于纯函数不依赖它的上下文环境,因此我们可以轻易的把它移植到任何地方运行它。</p>
<p>可测试性<br>我们不必在每次测试前都去配置和构造初始环境,只需简单给函数一个输入,然后断言它的输出就好了。</p>
<p>合理性<br>由于纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果,这也就保证了引用透明性。</p>
<p>并行执行<br>我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态。<br>并行代码在服务端 js 环境以及使用了 web worker 的浏览器那里是非常容易实现的,因为它们使用了线程(thread)。不过出于对非纯函数复杂度的考虑,当前主流观点还是避免使用这种并行。</p>
<h2>实现函数式编程的技术</h2>
<p>这里我们先不展开这些技术的细节内容,本文我们先侧重于对函数式编程有一个整体上的认识,具体的技术细节我们将在下一章展开。</p>
<ul>
<li>curry(柯里化)</li>
<li>compose(代码组合)</li>
<li>Monad(Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。)</li>
</ul>
<h2>如何正确看待函数式编程</h2>
<p>我们先来看以下几种观点:</p>
<ul>
<li>你这段代码用了 for 循环,这是过程式的。为了优雅,你应该写成函数式的。</li>
<li>你这段代码有副作用,这是肮脏的。为了纯净性,你应该把 IO 包在 Monad 里。</li>
<li>你这段代码用了 class,这是面向对象的。为了无状态,你应该写成高阶函数。</li>
</ul>
<p>我想说的是这种偏激的观点是不正确的,我们不应该把函数式编程和命令式编程对立起来,我们更多的时候需要考虑的是技术的适用场景。函数式编程写起代码来,有一定的难度,如果一个团队的整体水平达不到,那么写代码的质量和效率还不如采用命令式编程好。函数式编程利用纯函数的无状态性,它的好处非常多(结果可预期、利于测试、利于复用、利于并发),但一个系统工程的代码,是不可能全部采用纯函数来写的。当我们越贴近业务,我们就离纯函数与无状态越远。</p>
<p>函数式编程非常重要,学习它我们能打开我们的思维方式,使用它也有很多好处,但它也有一些局限,我们应该客观看待。保持开放的心态,根据实际场景选择合适的技术,是一个工程师基本的素养。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=OBx9eBRUxQ02FUvWHnzauQ%3D%3D.wxDCvVn5PD3awwybdazi0PsmuAC9bdRAuHiYGvsQ658VDdF6cEO%2BMYijhBcdeBH%2Bj7fkxizaDrPAf9YBO3Pf0IoVRmG6XpjSvQ3qMDyEqbk%3D" rel="nofollow">https://llh911001.gitbooks.io...</a><br><a href="https://link.segmentfault.com/?enc=MZGBElEhzmRYd1CE329XSg%3D%3D.my9Nj3z9pEAIBwfU5ci093x5oV0dWA1vfsbRuCzHcWunRehianKO4wWGcIPkR7BlysbBcnb8ij6SZ0Qv9D%2FCAQ%3D%3D" rel="nofollow">http://www.ruanyifeng.com/blo...</a><br><a href="https://link.segmentfault.com/?enc=b%2BmiznikjJlUnlRVO6E4Lg%3D%3D.hkWmcnJgFuR5zq82F2Cc7%2FGMdqLe9lm2P1NxlXimyzDwPNxjLy3UuwnTjtOxo1Pi" rel="nofollow">https://coolshell.cn/articles...</a><br><a href="https://link.segmentfault.com/?enc=eKZjiwW8pdEsYaN4zUl2zw%3D%3D.Cbz6Doo8O%2BRfEFITKrAtKDxv2w8fHSO7%2FbxTUnq2M2m8DpaWc9liQXUg60XRQf5ID9ZHXOAzRP4W5wRRbjpO%2Fw%3D%3D" rel="nofollow">https://www.zhihu.com/questio...</a><br><a href="https://link.segmentfault.com/?enc=yREEV3i217gJKocysB%2BMAw%3D%3D.We2WfLbvxptNm83A3EsmWEJZrVTSRv3ROmdJ72OQTQdKMBylnWgnQjaCqWhiPlSo" rel="nofollow">https://zhuanlan.zhihu.com/p/...</a></p>
React Fiber 原理介绍
https://segmentfault.com/a/1190000018250127
2019-02-22T15:32:54+08:00
2019-02-22T15:32:54+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
242
<p>欢迎关注我的公众号<code>睿Talk</code>,获取我最新的文章:<br><img src="https://segmentfault.com/img/bVbmYjo" alt="clipboard.png" title="clipboard.png"></p>
<h3>一、前言</h3>
<p>在 React Fiber 架构面世一年多后,最近 React 又发布了最新版 16.8.0,又一激动人心的特性:React Hooks 正式上线,让我升级 React 的意愿越来越强烈了。在升级之前,不妨回到原点,了解下人才济济的 React 团队为什么要大费周章,重写 React 架构,而 Fiber 又是个什么概念。</p>
<h3>二、React 15 的问题</h3>
<p>在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。请看以下例子:<br><a href="https://link.segmentfault.com/?enc=flgwoa4iTRxo84%2BFO7VCIQ%3D%3D.m%2FfV1QRxR1VguWYdOKJRXMpvB6b%2BUd4jd0%2BEz7sByz7OYgmIX92udFmdBNhNPFYXNV0gHqoEjcAvj3%2Bd54pJbQ%3D%3D" rel="nofollow">https://claudiopro.github.io/...</a></p>
<p><img src="/img/bVboH8j?w=550&h=280" alt="clipboard.png" title="clipboard.png"></p>
<p>其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用<code>setState</code>更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。</p>
<p>针对这一问题,React 团队从框架层面对 web 页面的运行机制做了优化,得到很好的效果。</p>
<p><img src="/img/bVboIan?w=550&h=280" alt="clipboard.png" title="clipboard.png"></p>
<h3>三、解题思路</h3>
<p>解决主线程长时间被 JS 运算占用这一问题的基本思路,是将运算切割为多个步骤,分批完成。也就是说在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。</p>
<p>旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而<code>Fiber</code>实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的<code>requestIdleCallback</code>这一 API。官方的解释是这样的:</p>
<blockquote>window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。</blockquote>
<p>有了解题思路后,我们再来看看 React 具体是怎么做的。</p>
<h3>四、React 的答卷</h3>
<p>React 框架内部的运作可以分为 3 层:</p>
<ul>
<li>Virtual DOM 层,描述页面长什么样。</li>
<li>Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。</li>
<li>Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。</li>
</ul>
<p>这次改动最大的当属 Reconciler 层了,React 团队也给它起了个新的名字,叫<code>Fiber Reconciler</code>。这就引入另一个关键词:Fiber。</p>
<p>Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:</p>
<pre><code class="javascript">const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}</code></pre>
<p>为了加以区分,以前的 Reconciler 被命名为<code>Stack Reconciler</code>。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑:</p>
<p><img src="/img/bVboIrF?w=1556&h=602" alt="clipboard.png" title="clipboard.png"></p>
<p>而 Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行:</p>
<p><img src="/img/bVboJj4?w=1472&h=578" alt="clipboard.png" title="clipboard.png"></p>
<p>为了达到这种效果,就需要有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:</p>
<ul>
<li>synchronous,与之前的Stack Reconciler操作一样,同步执行</li>
<li>task,在next tick之前执行</li>
<li>animation,下一帧之前执行</li>
<li>high,在不久的将来立即执行</li>
<li>low,稍微延迟执行也没关系</li>
<li>offscreen,下一次render时或scroll时才执行</li>
</ul>
<p>优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。</p>
<p>Fiber Reconciler 在执行过程中,会分为 2 个阶段。</p>
<p><img src="/img/bVboJH6?w=1076&h=697" alt="clipboard.png" title="clipboard.png"></p>
<ul>
<li>阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。</li>
<li>阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。</li>
</ul>
<p>阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。</p>
<h3>五、Fiber 树</h3>
<p>Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。</p>
<p><img src="/img/bVboJHa?w=970&h=732" alt="clipboard.png" title="clipboard.png"></p>
<p>Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程:</p>
<p><img src="/img/bVboJNB?w=872&h=785" alt="clipboard.png" title="clipboard.png"></p>
<p>如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。</p>
<p>在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在<code>Effect List</code>当中,在阶段二执行的时候,会批量更新相应的节点。</p>
<h3>六、总结</h3>
<p>本文从 React 15 存在的问题出发,介绍 React Fiber 解决问题的思路,并介绍了 Fiber Reconciler 的工作流程。从<code>Stack Reconciler</code>到<code>Fiber Reconciler</code>,源码层面其实就是干了一件递归改循环的事情,日后有机会的话,我再结合源码作进一步的介绍。</p>
项目管理培训总结
https://segmentfault.com/a/1190000018144698
2019-02-13T22:34:35+08:00
2019-02-13T22:34:35+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
9
<p>欢迎关注我的公众号<code>睿Talk</code>,获取我最新的文章:<br><img src="https://segmentfault.com/img/bVbmYjo" alt="clipboard.png" title="clipboard.png"></p>
<h3>一、前言</h3>
<p>年前公司组织了一次项目管理培训,激活了几年前考 PMP 的一些回忆。结合这两年来项目管理的实战经验,又有很多新的体会,这就是所谓的温故知新吧。学习过程中的一些想法总结成这篇文章,等有了新的感悟再继续扩展。</p>
<h3>二、人不能闲着</h3>
<p>培训开始前来了个热身思考题,题目如下:</p>
<blockquote>有拖地、擦窗和切菜 3 个任务,每个任务需要一个人 30 分钟的工作量。每个任务只有唯一的工具(拖把、抹布、刀),2 个人完成这 3 个任务最短需要多少时间。</blockquote>
<p>这个题目的关键是要把任务进行合理的拆分,让 2 个人都有事做,不能有一人闲下来。解题思路比较简单,每个 30 分钟的任务都拆成 2 个 15 分钟的子任务,然后将任务分成 3 组,每组 15 分钟 2 个并行进行的任务。所以最短时间是 45 分钟。</p>
<p>这个题目的场景在真实项目中也经常遇到,抽象来说就是在确定的项目范围和人员的情况下,如何更快的完成项目。以年前的一个项目为例,投入了 3 个前端小伙伴进行开发,估时的时候发现项目的最长路径在前端,而且其中一个人的工作量明显比其他 2 人多出一截。也就是说到项目后期,就一个人在忙,其他人都闲着了。解决方法跟这个题目的解题思路类似,就是将任务拆细,然后分给另外的 2 人,最终计划的完成时间也随之缩短了 2 天。</p>
<p>这种方法虽然能缩短计划工期,但在执行阶段可能出现其它问题,比如分出去的工作别人可能没那么熟悉,协作的时候需要更多的沟通。代码质量也可能会降低,出现更多的 bug。这些风险都要提前识别出来,然后制定相应的应对计划。</p>
<h3>三、目标管理</h3>
<p>课程开始的时候提出了一个引子:西游记 4 师徒的目标是什么?很多人都不假思索的说是到西天拜佛求经。唐僧自己在化缘和借宿的时候不也是这么说的吗?但这是真正的目标吗?显然不是。这个项目的真正目标应该是普度众生,拜佛求经只是一种方式,或者说是一个重要的里程碑。再细想一下,其实师徒 4 人每个人的目标都是不一样的,孙悟空只想从五指山下出来,八戒就是个贪图美色没有目标的吃货,而沙僧只想混点资历好日后升官。</p>
<p>目标管理对一个项目的成败有重要的影响,如果能将项目成员的个人目标跟项目目标统一起来,就能最大限度的激发他们的主观能动性,推动项目的顺利进行。还是以年前的项目为例子,产品的目标是上线新功能以满足商家的需求,客户成功的目标是提高商家的日活,而技术的目标是满足需求的基础上完成代码重构,方便代码日后的维护。要达成每个项目干系人的目标,需要前期不断的调频、协商,某些情况下为了达成项目目标需要某些人的妥协,比如工期太紧就下次再重构。把目标管理的工作做好了,在执行阶段会起到事半功倍的效果。</p>
<h3>四、期望管理</h3>
<p>接着我们就分组进行主题为婚礼的项目规划,有多个限制条件,其中一个是丈母娘要满意。在着手制定方案的时候,我们组内一直在猜想丈母娘的需求是什么,怎么样的婚礼是她想要的,没有一个人提出说要跟丈母娘聊聊,听听她的意见。这其实是项目管理的一个大忌。对重要的项目干系人,一定要事先做好沟通,了解需求,并根据项目的客观情况来管理他们的期望。如果期望没管理好,最终的结果很可能是辛辛苦苦把项目完成了,自我感觉不错,但干系人却并不满意。</p>
<p>之前就做过一个项目,下了死命令一定要在某天发布。过程中费了好大的力气,连续加班将近一个月,项目终于如期上线了。就在小伙伴们以为可以松一口气的时候,收到了客户们如潮水般的吐槽。由于新版变化很大,并且是一个高频的功能,客户已有的操作习惯完全被打破,客户成功疲于应付各种质问,无论对内还是对外都产生了不良的影响。现在回想起来,其实就是没对终端用户这群重要干系人做好期望管理。如果在项目发布前一周甚至是前一天,提前跟他们做好沟通,预告接下来将发生的变化和提供一些新版操作的指引,相信会有完全不一样的结果。</p>
<h3>五、领导力</h3>
<p>在聊项目经理技能树的时候,主要有 3 个要求:</p>
<ul>
<li>项目管理知识</li>
<li>领导力</li>
<li>业务知识</li>
</ul>
<p>其中我对领导力的感悟比较深。课堂上放了《可复制的领导力》视频,当中说到领导的核心驱动是尊敬和信任。一个人的领导力是否强大主要看能否做到以下几方面:</p>
<ul>
<li>树立共同的目标,给员工以目标感和价值感</li>
<li>有清晰明确的规则</li>
<li>及时反馈</li>
<li>自愿参与,帮助员工成长</li>
</ul>
<p>布置任务的时候,最好做到 5 个来回的沟通:</p>
<ul>
<li>第一遍,交代清楚事项;</li>
<li>第二遍,要求员工复述;</li>
<li>第三遍,和员工探讨此事项的目的;</li>
<li>第四遍,做应急预案;</li>
<li>第五遍,要求员工提出个人见解;</li>
</ul>
<p>这颠覆了我对领导力的认识,我以前认为一个领导力强的人必须能说会道,见多识广。但这些其实只是一些表面功夫,要提高自己的领导力,更需要深入到做事的细节当中。</p>
<h3>六、目标驱动</h3>
<p>在项目启动的时候,要先跟项目成员阐述项目的目标是什么,来源在哪里,解决什么问题。这部分可能花的时间不多,但对项目的顺利执行是很有益处的。描述得越具体,细节越丰富,作用就越大。最好让大家都能切身感受到痛点,从而激发大家改进的愿望。</p>
<p>在我们公司业务发展的早期,需求下来后只是着重讲解功能点是什么,并没有同步为什么要做这个,背后的产品逻辑是怎样的。结果就是大家都很被动的去完成功能,做得到底好不好,没有一个评判标准。随着业务的发展和项目管理经验的积累,这一问题得到了改善。产品经理在需求沟通的时候会先介绍做了哪些调研,哪些客户提出了需求,没这个功能对他们来说有多不方便。在了解这些背景资料后,往往会激发大家的目标感和使命感,更积极主动的去推进项目。迭代上线后,也可以聆听客户的反馈,形成一个完美的闭环。</p>
<h3>七、透明、检视、调整</h3>
<p>在敏捷项目的体系里面,看板是一个十分重要的存在。通过在看版上面罗列需求和展示对应的状态,可以让项目干系人很直观的看到需求所处的阶段和完成的情况,以透明的方式达到信息同步与鞭策的作用。</p>
<p>每天站会的时候,可以提前更新任务的状态,方便大家去检视项目的健康状况。当有需求变更或计划调整的时候,也可以直接反映到看版上,可谓一举多得。</p>
<h3>八、团队五阶段</h3>
<p>团队的生命周期会经历 5 个阶段,它们是:</p>
<ul>
<li>形成阶段。在本阶段,团队成员相互认识,并了解项目情况及他们在项目中的正式角色与职责。团队成员倾向于相互独立,不一定开诚布公。</li>
<li>震荡阶段。在本阶段,团队开始从事项目工作,制定技术决策和讨论项目管理方法。如果团队成员不能用合作和开放的态度对待不同观点和意见,团队环境可能变得事与愿违。</li>
<li>规范阶段。在规范阶段,团队成员开始协同工作,并调整各自的工作习惯和行为来支持团队,团队成员开始相互信任。</li>
<li>成熟阶段。进入这一阶段后,团队就像一个组织有序的单位那样工作。团队成员之间相互依靠,平稳高效地解决问题。</li>
<li>解散阶段。团队完成所有工作,团队成员离开项目。</li>
</ul>
<p>最近一年多我就完整的经历了这 5 个阶段,上面的总结都非常到位,深有体会。随着业务的发展,原有团队完成历史使命,解散后又成立一个新的团队。目前正处于团队形成的阶段,共同去完成新的挑战。</p>
<h3>九、5 Why</h3>
<p>当项目出现问题的时候,可以用 5 Why 这个思维工具来找到根本原因。凡事多问几个为什么,会得到意想不到的收获。比如:</p>
<ul>
<li>项目为什么会延期?因为开发时间太短了。</li>
<li>为什么开发时间短?因为开发过程中遇到了意想不到的问题。</li>
<li>为什么会遇到意想不到的问题?技术设计的时候想得不够详细。</li>
<li>为什么技术设计的时候想得不够详细?主观觉得这个需求很简单,没必要做详细的设计。</li>
<li>为什么简单的需求会遇到意想不到的问题?因为对代码不熟悉。</li>
<li>...</li>
</ul>
<p>通过这么一层一层的刨根问底,对问题的认识会更深刻,制定出的改进方案会更有效。</p>
<h3>十、项目管理铁三角</h3>
<p>范围、成本和时间构成项目管理的铁三角,而项目质量跟三方都有密切关系,任何一方的变动都会对另外几方面产生影响。比如:</p>
<ul>
<li>范围确定的情况下,要缩短项目时间,就必须投入更多的资源,成本也随之上升。</li>
<li>如果要缩短项目时间,但成本不变,就必须调整项目范围,也就是所谓的砍需求。</li>
<li>当项目进行的过程中,如果有需求变更,范围扩大了,人员不变的情况下就需要增加时间。</li>
</ul>
<h3>十一、总结</h3>
<p>以上就是这次课程的一些感悟,也是真实项目中给我的切身体会。项目管理其实就是一套方法论和工具体系,它不但可以在工作中发挥作用,还可以应用到日常生活之中,通过周密的计划,对人、事、物做好管理,从而取得满意的结果。</p>
React 最佳实践
https://segmentfault.com/a/1190000018107137
2019-02-08T01:14:24+08:00
2019-02-08T01:14:24+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
61
<p>欢迎关注我的公众号<code>睿Talk</code>,获取我最新的文章:<br><img src="https://segmentfault.com/img/bVbmYjo" alt="clipboard.png" title="clipboard.png"></p>
<h3>一、前言</h3>
<p>在日常开发和 Code Review 的时候,常常会发现一些共性的问题,也有很多值得提倡的做法。本文针对 React 技术栈,总结了一些最佳实践,对编写高质量的代码有一定的参考作用。</p>
<h3>二、最佳实践 & 说明</h3>
<ul><li><h4>多用 Function Component</h4></li></ul>
<p>如果组件是纯展示型的,不需要维护 state 和生命周期,则优先使用 Function Component。它有如下好处:</p>
<ol>
<li>代码更简洁,一看就知道是纯展示型的,没有复杂的业务逻辑</li>
<li>更好的复用性。只要传入相同结构的 props,就能展示相同的界面,不需要考虑副作用。</li>
<li>更小的打包体积,更高的执行效率</li>
</ol>
<p>一个典型的 Function Component 是下面这个样子:</p>
<pre><code class="javascript">function MenuItem({menuId, menuText, onClick, activeId}) {
return (
<div
menuId={menuId}
className={`${style} ${activeId === menuId ? active : ''}`}
onClick={onItemClick}
>
{menuText}
</div>
);
};</code></pre>
<ul><li><h4>多用 PureComponent</h4></li></ul>
<p>如果组件需要维护 state 或使用生命周期方法,则优先使用 PureComponent,而不是 Component。Component 的默认行为是不论 state 和 props 是否有变化,都触发 render。而 PureComponent 会先对 state 和 props 进行浅比较,不同的时候才会 render。请看下面的例子:</p>
<pre><code class="javascript">class Child extends React.Component {
render() {
console.log('render Child');
return (
<div>
{this.props.obj.num}
</div>
);
}
}
class App extends React.Component {
state = {
obj: { num: 1 }
};
onClick = () => {
const {obj} = this.state;
this.setState({obj});
}
render() {
console.log('render Parent');
return (
<div className="App" >
<button onClick={this.onClick}>
点我
</button>
<Child obj={this.state.obj}/>
</div>
);
}
}</code></pre>
<p>点击按钮后,Parent 和 Child 的 render 都会触发。如果将 Child 改为 PureComponent,则 Child 的 render 不会触发,因为 props 还是同一个对象。如果将 Parent 也改为 PureComponent,则 Parent 的 render 也不会触发了,因为 state 还是同一个对象。</p>
<ul><li><h4>遵循单一职责原则,使用 HOC / 装饰器 / Render Props 增加职责</h4></li></ul>
<p>比如一个公用的组件,数据来源可能是父组件传过来,又或者是自己主动通过网络请求获取数据。这时候可以先定义一个纯展示型的 Function Component,然后再定义一个高阶组件去获取数据:</p>
<pre><code class="javascript">function Comp() {
...
}
class HOC extends PureComponent {
async componentDidMount() {
const data = await fetchData();
this.setState({data});
}
render() {
return (<Comp data={this.state.data}/>);
}
}</code></pre>
<ul><li><h4>组合优于继承</h4></li></ul>
<p>笔者在真实项目中就试过以继承的形式写组件,自己写得很爽,代码的复用性也很好,但最大的问题是别人看不懂。我将复用的业务逻辑和 UI 模版都在父类定义好,子类只需要传入一些参数,然后再覆盖父类的几个方法就好(render的时候会用到)。简化的代码如下:</p>
<pre><code class="javascript">class Parent extends PureComponent {
componentDidMount() {
this.fetchData(this.url);
}
fetchData(url) {
...
}
render() {
const data = this.calcData();
return (
<div>{data}</data>
);
}
}
class Child extends Parent {
constructor(props) {
super(props);
this.url = 'http://api';
}
calcData() {
...
}
}</code></pre>
<p>这样的写法从语言的特性和功能实现来说,没有任何问题,最大的问题是不符合 React 的组件编写习惯。父类或者子类肯定有一方是不需要实现 render 方法的,而一般我们看代码都会优先找 render 方法,找不到就慌了。另外就是搞不清楚哪些方法是父类实现的,哪些方法是子类实现的,如果让其他人来维护这份代码,会比较吃力。</p>
<p>继承会让代码难以溯源,定位问题也比较麻烦。所有通过继承实现的组件都可以改写为组合的形式。上面的代码就可以这样改写:</p>
<pre><code class="javascript">class Parent extends PureComponent {
componentDidMount() {
this.fetchData(this.props.url);
}
fetchData(url) {
...
}
render() {
const data = this.props.calcData(this.state);
return (
<div>{data}</data>
);
}
}
class Child extends PureComponent {
calcData(state) {
...
}
render() {
<Parent url="http://api" calcData={this.calcData}/>
}
}</code></pre>
<p>这样的代码是不是看起来舒服多了?</p>
<ul><li><h4>如果 props 的数据不会改变,就不需要在 state 或者组件实例属性里拷贝一份</h4></li></ul>
<p>经常会看见这样的代码:</p>
<pre><code class="javascript">componentWillReceiveProps(nextProps) {
this.setState({num: nextProps.num});
}
render() {
return(
<div>{this.state.num}</div>
);
}</code></pre>
<p>num 在组件中不会做任何的改变,这种情况下直接使用 this.props.num 就可以了。</p>
<ul><li><h4>避免在 render 里面动态创建对象 / 方法,否则会导致子组件每次都 render</h4></li></ul>
<pre><code class="javascript">render() {
const obj = {num: 1}
return(
<Child obj={obj} onClick={()=>{...}} />
);
}</code></pre>
<p>在上面代码中,即使 Child 是 PureComponent,由于 obj 和 onClick 每次 render 都是新的对象,Child 也会跟着 render。</p>
<ul><li><h4>避免在 JSX 中写复杂的三元表达式,应通过封装函数或组件实现</h4></li></ul>
<pre><code class="javascript">render() {
const a = 8;
return (
<div>
{
a > 0 ? a < 9 ? ... : ... : ...
}
</div>
);
}</code></pre>
<p>像上面这种嵌套的三元表达式可读性非常差,可以写成下面的形式:</p>
<pre><code class="javascript">f() {
...
}
render() {
const a = 8;
return (
<div>
{
this.f()
}
</div>
);
}</code></pre>
<ul><li><h4>多使用解构,如 Function Component 的 props</h4></li></ul>
<pre><code class="javascript">const MenuItem = ({
menuId, menuText, onClick, activeId,
}) => {
return (
...
);
};</code></pre>
<ul><li><h4>定义组件时,定义 PropTypes 和 defaultProps</h4></li></ul>
<p>例子如下:</p>
<pre><code class="javascript">class CategorySelector extends PureComponent {
...
}
CategorySelector.propTypes = {
type: PropTypes.string,
catList: PropTypes.array.isRequired,
default: PropTypes.bool,
};
CategorySelector.defaultProps = {
default: false,
type: undefined,
};</code></pre>
<ul><li><h4>避免使用无谓的标签和样式</h4></li></ul>
<p>下面这种情况一般外层的<code>div</code>是多余的,可以将样式直接定义在组件内,或者将定制的样式作为参数传入。例外:当<code>ServiceItem</code>需要在多个地方使用,而且要叠加很多不一样的样式,原写法会方便些。</p>
<pre><code class="javascript">// bad
<div key={item.uuid} className={scss.serviceItemContainer}>
<ServiceItem item={item} />
</div>
// good
<ServiceItem key={item.uuid} item={item} className={customStyle} /></code></pre>
<h3>三、总结</h3>
<p>本文列举了笔者在项目实战和 Code Review 过程中总结的 10 条最佳实践,当中的一些写法和原则只代表个人立场。理解并遵循这些最佳实践,写出来的代码质量会有一定的保证。如果你有不同的意见,或者有补充的最佳实践,欢迎留言交流。</p>
装饰器与元数据反射(4)元数据反射
https://segmentfault.com/a/1190000018094145
2019-02-02T19:19:11+08:00
2019-02-02T19:19:11+08:00
sept08
https://segmentfault.com/u/sept08
4
<p>本篇内容包括如下部分:</p>
<ol>
<li>为什么JavaScript中需要反射</li>
<li>元数据反射API</li>
<li>基本类型序列化</li>
<li>复杂类型序列化</li>
</ol>
<h2>为什么JavaScript中需要反射?</h2>
<p>关于反射的概念,摘自<a href="https://link.segmentfault.com/?enc=ioyZ39J86OOWc6zgnBLPFw%3D%3D.SMeEBLuQDGwFy4Un8ItXeKPm%2FQtNkqqyLcktFsclvqrAPpXmGey1c1nDQ5RBI9OWwGHTsfOSwUHk6qFKmfjGtjw802AWaNwUfu3Yd0AmG10%3D" rel="nofollow">百度百科</a></p>
<blockquote>在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。</blockquote>
<p>可见反射机制对于依赖注入、运行时类型断言、测试是非常有用的,同时随着基于JavaScript的应用做的越来越大,使得我们希望有一些工具和特性可以用来应对增长的复杂度,例如控制反转,运行时类型断言等。但由于JavaScript语言中没有反射机制,所以导致这些东西要么没法实现,要么实现的不如<code>C#</code>或<code>Java</code>语言实现的强大。</p>
<p>强大的反射API允许我们可以在运行时测试一个未知的类,以及找到关于它的任何信息,包括:名称、类型、接口等。虽然可以使用诸如<code>Object.getOwnPropertyDescriptor()</code>和<code>Object.keys()</code>查询到一些信息,但我们需要反射来实现更强大的开发工具。庆幸的是,TypeScript已经支持反射机制,来看看这个特性吧</p>
<h2>元数据反射API</h2>
<p>可以通过安装<code>reflect-metadata</code>包来使用元数据反射的API</p>
<pre><code>npm install reflect-metadata;</code></pre>
<p>若要使用它,我们需要在<code>tsconfig.json</code>中设置<code>emitDecoratorMetadata</code>为<code>true</code>,同时添加<code>reflect-metadata.d.ts</code>的引用,同时加载<code>Reflect.js</code>文件。然后我们来实现装饰器并使用反射元数据设计的键值,目前可用的有:</p>
<ul>
<li>类型元数据:<code>design:type</code>
</li>
<li>参数类型元数据:<code>design:paramtypes</code>
</li>
<li>返回类型元数据:<code>design:returntype</code>
</li>
</ul>
<p>我们来通过一组例子来说明</p>
<h2>1)获取类型元数据</h2>
<p>首先声明如下的属性装饰器:</p>
<pre><code>function logType(target : any, key : string) {
var t = Reflect.getMetadata("design:type", target, key);
console.log(`${key} type: ${t.name}`);
}</code></pre>
<p>接下来将其应用到一个类的属性上,以获取其类型:</p>
<pre><code>class Demo{
@logType
public attr1 : string;
}</code></pre>
<p>这个例子将会在控制台中打印如下信息:</p>
<pre><code class="bash">attr1 type: String</code></pre>
<h2>2) 获取参数类型元数据</h2>
<p>声明参数装饰器如下:</p>
<pre><code>function logParamTypes(target : any, key : string) {
var types = Reflect.getMetadata("design:paramtypes", target, key);
var s = types.map(a => a.name).join();
console.log(`${key} param types: ${s}`);
} </code></pre>
<p>然后将它应用在一个类方法的参数上,用以获取所装饰参数的类型:</p>
<pre><code>class Foo {}
interface IFoo {}
class Demo{
@logParameters
param1 : string,
param2 : number,
param3 : Foo,
param4 : { test : string },
param5 : IFoo,
param6 : Function,
param7 : (a : number) => void,
) : number {
return 1
}
}</code></pre>
<p>这个例子的执行结果是:</p>
<pre><code class="bash">doSomething param types: String, Number, Foo, Object, Object, Function, Function</code></pre>
<h2>3) 获取返回类型元数据</h2>
<p>同样的我们可以使用<code>"design:returntype"</code>元数据键值,来获取一个方法的返回类型:</p>
<pre><code>Reflect.getMetadata("design:returntype", target, key);</code></pre>
<h2>基本类型序列化</h2>
<p>让我们回看上面关于<code>"design:paramtypes"</code>的例子,注意到接口<code>IFoo</code>和对象字面量<code>{test: string}</code>被序列化为<code>Object</code>,这是因为TypeScript仅支持基本类型的序列化,基本类型序列化规则如下:</p>
<ul>
<li>
<code>number</code>序列化为<code>Number</code>
</li>
<li>
<code>string</code>序列化为<code>String</code>
</li>
<li>
<code>boolean</code>序列化为<code>Boolean</code>
</li>
<li>
<code>any</code>序列化为<code>Object</code>
</li>
<li>
<code>void</code>序列化为<code>undefined</code>
</li>
<li>
<code>Array</code>序列化为<code>Array</code>
</li>
<li>元组<code>Tuple</code>序列化为<code>Array</code>
</li>
<li>类<code>class</code>序列化为类的构造函数</li>
<li>枚举<code>Enum</code>序列化为<code>Number</code>
</li>
<li>剩下的所有其他类型都被序列化为<code>Object</code>
</li>
</ul>
<p>接口和对象字面量可能在之后的复杂类型序列化中会被做具体的处理。</p>
<h2>复杂类型序列化</h2>
<p>TypeScript的团队为复杂类型的元数据序列化做出了努力。上面列出的序列化规则对基本类型依然适用,但对复杂类型提出了不同的序列化逻辑。如下是通过一个例子来描述所有可能的类型:</p>
<pre><code>interface _Type {
/**
* Describes the specific shape of the type.
* @remarks
* One of: "typeparameter", "typereference", "interface", "tuple", "union" or "function".
*/
kind: string;
}</code></pre>
<p>我们也可以找到用于描述每种可能类型的类,例如用于序列化通用接口<code>interface foo<bar></code>:</p>
<pre><code>// 描述一个通用接口
interface InterfaceType extends _Type {
kind: string; // "interface"
// 通用类型参数. 可能为undefined.
typeParameters?: TypeParameter[];
// 实现的接口.
implements?: Type[];
// 类型的成员 可能为undefined.
members?: { [key: string | symbol | number]: Type; };
// 类型的调用标识. 可能为undefined.
call?: Signature[];
// 类型的构造标识. 可能为undefined.
construct?: Signature[];
// 类型的索引标识. 可能为undefined.
index?: Signature[];
}</code></pre>
<p>这里有一个属性指出实现了哪些接口</p>
<pre><code>// 实现的接口
implements?: Type[];</code></pre>
<p>这种信息可以用来在运行时验证一个实例是否实现了特定的接口,而这个功能对于一个依赖翻转容器特别的有用。</p>
装饰器与元数据反射(3)参数装饰器
https://segmentfault.com/a/1190000018092722
2019-02-02T15:10:34+08:00
2019-02-02T15:10:34+08:00
sept08
https://segmentfault.com/u/sept08
4
<p>之前已经分别介绍了<a href="https://segmentfault.com/a/1190000018087124">方法装饰器</a>、<a href="https://segmentfault.com/a/1190000018091354">属性装饰器和类装饰器</a>,这篇文章我们来继续关注这些话题:</p>
<ul>
<li>参数装饰器</li>
<li>装饰器工厂</li>
</ul>
<p>我们将围绕以下这个例子,来探讨这些概念:</p>
<pre><code>class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
public saySomething(something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}</code></pre>
<h2>参数装饰器</h2>
<p>TypeScript对于参数装饰器的声明如下</p>
<pre><code>declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;</code></pre>
<p>如下我们为类<code>Person</code>的<code>saySomething</code>方法的参数添加一个参数装饰器</p>
<pre><code>public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}</code></pre>
<p>最终被编译为JavaScript的样子为:</p>
<pre><code>Object.defineProperty(Person.prototype, "saySomething",
__decorate(
[__param(0, logParameter)],
Person.prototype,
"saySomething",
Object.getOwnPropertyDescriptor(Person.prototype, "saySomething")
)
);
return Person;</code></pre>
<p>如果将其和之前的装饰器比较,是否会发现又使用了<code>Object.defineProperty()</code>方法,那么是否意味着<code>saySomething</code>将被<code>__decorated</code>函数的返回值替换?</p>
<p>我们发现这里有个新函数<code>__param</code>,TypeScript编译器生成如下:</p>
<pre><code>var __param = this.__param || function (index, decorator) {
// 返回一个装饰器函数
return function (target, key) {
// 应用装饰器(忽略返回值)
decorator(target, key, index);
}
};</code></pre>
<p>如上所示,调用参数装饰器,其并没有返回值,这就意味着,函数<code>__decorate</code>的调用返回并没有覆盖方法<code>saySomething</code>,也很好理解:参数装饰器要毛返回。</p>
<p>可见参数装饰器函数需要3个参数:被装饰类的原型,装饰参数所属的方法名,参数的索引。具体的实现如下:</p>
<pre><code>function logParameter(target: any, key : string, index : number) {
var metadataKey = `log_${key}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
}
else {
target[metadataKey] = [index];
}
}</code></pre>
<p>其中向类的原型中增加一个新的属性<code>metadataKey</code>,该属性值是一个数组,包含所装饰参数的索引,可以把它当作元数据。</p>
<p>参数装饰器不应当用来修改构造器、方法或属性的行为,它只应当用来产生某种元数据。一旦元数据被创建,我们便可以用其它的装饰器去读取它。</p>
<h2>装饰器工厂</h2>
<p>官方TypeScript装饰器建议定义一个如下的装饰器工厂:</p>
<blockquote>装饰器工厂首先是一个函数,它接受任意数量的参数,同时返回如前所述的四种之一特定类型的装饰器。</blockquote>
<p>虽然已经讨论四种装饰是如何实现及使用的,但还是有一些可以改进的地方,观察下面的代码片段:</p>
<pre><code>@logClass
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@logMethod
public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}</code></pre>
<p>这里装饰器的使用是没问题的,但如果我们可以不关心装饰器的类型,而在任何地方使用岂不方便,就像下面的样子:</p>
<pre><code>@log
class Person {
@log
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@log
public saySomething(@log something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}</code></pre>
<p>这边是装饰器工厂的使用诉求,它可以识别具体情况下该使用哪种类型的装饰器,幸运的是,我们可以通过传递给装饰器的参数来区分它的类型。</p>
<pre><code>function log(...args : any[]) {
switch(args.length) {
case 1:
return logClass.apply(this, args);
case 2:
return logProperty.apply(this, args);
case 3:
if(typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
default:
throw new Error("Decorators are not valid here!");
}
}</code></pre>
装饰器与元数据反射(2)属与类性装饰器
https://segmentfault.com/a/1190000018091354
2019-02-02T10:37:12+08:00
2019-02-02T10:37:12+08:00
sept08
https://segmentfault.com/u/sept08
2
<p><a href="https://segmentfault.com/a/1190000018087124">上一篇文章</a>中,我们讨论了TypeScript源码中关于方法装饰器的实现,搞明白了如下几个问题:</p>
<ul>
<li>装饰器函数是如何被调用的?</li>
<li>装饰器函数参数是如何传入的?</li>
<li>
<code>__decorate</code>函数干了些什么事情?</li>
</ul>
<p>接下来我们继续<strong>属性装饰器</strong>的观察。</p>
<h2>属性装饰器</h2>
<p>属性装饰器的声明标识如下:</p>
<pre><code>declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;</code></pre>
<p>如下我们为一个类的属性添加了一个名为<code>@logProperty</code>的装饰器</p>
<pre><code class="javascript">class Person {
@logProperty
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
}</code></pre>
<p>上一篇解释过,当这段代码最后被编译成JavaScript执行时,方法<code>__decorate</code>会被调用,但此处会少最后一个参数(通过<code>Object. getOwnPropertyDescriptor</code>属性描述符)</p>
<pre><code>var Person = (function () {
function Person(name, surname) {
this.name = name;
this.surname = surname;
}
__decorate(
[logProperty],
Person.prototype,
"name"
);
return Person;
})();</code></pre>
<p>需要注意的是,这次TypeScript编译器并没像方法装饰器那样,使用<code>__decorate</code>返回的结果覆盖原始属性。原因是属性装饰器并不需要返回什么。</p>
<pre><code>Object.defineProperty(C.prototype, "foo",
__decorate(
[log],
C.prototype,
"foo",
Object.getOwnPropertyDescriptor(C.prototype, "foo")
)
);</code></pre>
<p>那么,接下来具体实现这个<code>@logProperty</code>装饰器</p>
<pre><code>function logProperty(target: any, key: string) {
// 属性值
var _val = this[key];
// getter
var getter = function () {
console.log(`Get: ${key} => ${_val}`);
return _val;
};
// setter
var setter = function (newVal) {
console.log(`Set: ${key} => ${newVal}`);
_val = newVal;
};
// 删除属性
if (delete this[key]) {
// 创建新的属性
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}</code></pre>
<p>实现过程首先声明了一个变量<code>_val</code>,并用所装饰的属性值给它赋值(此处的<code>this</code>指向类的原型,<code>key</code>为属性的名字)。</p>
<p>接着声明了两个方法<code>getter</code>和<code>setter</code>,由于函数是闭包创建的,所以在其中可以访问变量<code>_val</code>,在其中可以添加额外的自定义行为,这里添加了将属性值打印在控制台的操作。</p>
<p>然后使用<code>delete</code>操作符将原属性从类的原型中删除,不过需要注意的是:如果属性存在不可配置的属性时,这里<code>if(delete this[key])</code>会返回false。而当属性被成功删除,方法<code>Object.defineProperty()</code>将创建一个和原属性同名的属性,不同的是新的属性<code>getter</code>和<code>setter</code>方法,使用上面新创建的。</p>
<p>至此,属性装饰器的实现就完成了,运行结果如下:</p>
<pre><code>var me = new Person("Remo", "Jansen");
// Set: name => Remo
me.name = "Remo H.";
// Set: name => Remo H.
name;
// Get: name Remo H.</code></pre>
<h2>类装饰器</h2>
<p>类装饰器的声明标识如下:</p>
<pre><code>declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;</code></pre>
<p>可以像如下方式使用类装饰器:</p>
<pre><code>@logClass
class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
}</code></pre>
<p>和之前不同的是,经过TypeScript编译器编译为JavaScript后,调用<code>__decorate</code>函数时,与方法装饰器相比少了后两个参数。仅传递了<code>Person</code>而非<code>Person.prototype</code>。</p>
<pre><code>var Person = (function () {
function Person(name, surname) {
this.name = name;
this.surname = surname;
}
Person = __decorate(
[logClass],
Person
);
return Person;
})();</code></pre>
<p>值得注意的是,<code>__decorate</code>的返回值复写了原始的构造函数,原因是类装饰器必须返回一个构造器函数。接下来我们就来实现上面用到的类装饰器<code>@logClass</code>:</p>
<pre><code>function logClass(target: any) {
// 保存对原始构造函数的引用
var original = target;
// 用来生成类实例的方法
function construct(constructor, args) {
var c : any = function () {
return constructor.apply(this, args);
}
c.prototype = constructor.prototype;
return new c();
}
// 新的构造函数
var f : any = function (...args) {
console.log("New: " + original.name);
return construct(original, args);
}
// 复制原型以便`intanceof`操作符可以使用
f.prototype = original.prototype;
// 返回新的构造函数(会覆盖原有构造函数)
return f;
}</code></pre>
<p>这里实现的构造器中,声明了一个名为<code>original</code>的变量,并将所装饰类的构造函数赋值给它。接着声明一个工具函数<code>construct</code>,用来创建类的实例。然后定义新的构造函数<code>f</code>,在其中调用原来的构造函数并将初始化的类名打印在控制台,当然我们也可以添加一些其他自定义的行为。</p>
<p>原始构造函数的原型被复制给<code>f</code>的原型,以确保在创建一个<code>Person</code>的新实例时,<code>instanceof</code>操作符如愿以偿,具体原因可参考鄙人另一篇文章<a href="https://segmentfault.com/a/1190000014515674">原型与对象</a>。</p>
<p>至此类装饰器的实现就完成了,可以验证下:</p>
<pre><code>var me = new Person("Remo", "Jansen");
// New: Person
me instanceof Person;
// true</code></pre>
装饰器与元数据反射(1)方法装饰器
https://segmentfault.com/a/1190000018087124
2019-02-01T15:00:03+08:00
2019-02-01T15:00:03+08:00
sept08
https://segmentfault.com/u/sept08
6
<p>让我来深入地了解一下TypeScript对于装饰器模式的实现,以及反射与依赖注入等相关特性。</p>
<p>在<code>Typescript</code>的<a href="https://link.segmentfault.com/?enc=RQBa%2FGIEnZwjQohaax2b9A%3D%3D.1Z2LrXFW9I8gxgrweFC0VIh%2Fg0elwrIwQ4pfLSzcdBtQeY6TBFG7yF6Gluo9R4RtezMxxOcTwRdPFVp4B%2F8Apczaxi05j44rqiL7Z6E%2BKS8%3D" rel="nofollow">源代码</a>中,可以看到装饰器能用来修饰<code>class</code>,<code>property</code>,<code>method</code>,<code>parameter</code>:</p>
<pre><code>declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;</code></pre>
<p>接下来深入地了解一下每种装饰器:</p>
<h2>方法装饰器</h2>
<p>首先来根据上面的标识,实现一个名为<code>log</code>的方法装饰器。使用装饰器的方法很简单:在装饰器名前加<code>@</code>字符,写在想要装饰的方法上,类似写注释的方式:</p>
<pre><code class="javascript">class C {
@log
foo(n: number) {
return n * 2;
}
}</code></pre>
<p>装饰器实际上是一个函数,入参为所装饰的方法,返回值为装饰后的方法。在使用之前需要提前实现这个装饰器函数,如下:</p>
<pre><code class="javascript">function log(target: Function, key: string, descriptor: any) {
// target === C.prototype
// key === "foo"
// descriptor === Object.getOwnPropertyDescriptor(C.prototype, "foo")
// 保存对原方法的引用,避免重写
var originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// 将“foo”函数的参数列表转化为字符串
var a = args.map(a => JSON.stringify(a)).join();
// 调用 foo() 并获取它的返回值
var result = originalMethod.apply(this, args);
// 将返回的结果转成字符串
var r = JSON.stringify(result);
// 打印日志
console.log(`Call: ${key}(${a}) => ${r}`);
// 返回调用 foo 的结果
return result;
}
// 返回已编辑的描述符
return descriptor;
}</code></pre>
<p>该装饰器函数包含三个参数:</p>
<ul>
<li>
<code>target</code>:所要修饰的方法。</li>
<li>
<code>key</code>:被修饰方法的名字。</li>
<li>
<code>descriptor</code>:属性描述符,如果为给定可以通过调用<code>Object.getOwnPropertyDescriptor()</code>来获取。</li>
</ul>
<p>我们观察到,类<code>C</code>中使用的装饰器函数<code>log</code>并没有显式的参数传递,不免好奇<strong>它所需要的参数是如何传递的?</strong>以及<strong>该函数是如何被调用的?</strong></p>
<p>TypeScript最终还是会被编译为JavaScript执行,为了搞清上面的问题,我们来看一下TypeScript编译器将类<code>C</code>的定义最终生成的JavaScript代码:</p>
<pre><code class="javascript">var C = (function () {
function C() {
}
C.prototype.foo = function (n) {
return n * 2;
};
Object.defineProperty(C.prototype, "foo",
__decorate([
log
], C.prototype, "foo", Object.getOwnPropertyDescriptor(C.prototype, "foo")));
return C;
})();</code></pre>
<p>而为添加装饰器所生成的JavaScript代码如下:</p>
<pre><code class="javascript">var C = (function () {
function C() {
}
C.prototype.foo = function (n) {
return n * 2;
};
return C;
})();</code></pre>
<p>对比两者发现使用装饰的不同,只是在类定义中,多了如下代码:</p>
<pre><code>Object.defineProperty(
__decorate(
[log], // 装饰器
C.prototype, // target:C的原型
"foo", // key:装饰器修饰的方法名
Object.getOwnPropertyDescriptor(C.prototype, "foo") // descriptor
);
);</code></pre>
<p>通过查询<a href="https://link.segmentfault.com/?enc=jmSAafW9jnfp5O%2B479CuHA%3D%3D.V%2FrAOpd8kFCNqOeB6XNHzDvIhf9IIEivAe9aPeRS8NuGUENRJw0qhrd4WJpFuhtirGSn6wHeHEmJ%2BPJBEZ9AZLRphmDV6A%2FXKIiFH%2FyQw2UGKgwwvjDqWrXVR%2FTTCXs09meEUhy86IAlFiLXjSVCfg%3D%3D" rel="nofollow">MDN文档</a>,可以知悉<code>defineProperty</code>的作用:</p>
<blockquote>
<code>Object.defineProperty()</code>方法可直接在一个对象上定义一个新的属性,或者修改对象上一个已有的属性,然后返回这个对象。</blockquote>
<p>TypeScript编译器通过<code>defineProperty</code>方法重写了所修饰的方法<code>foo</code>,新方法的实现是由函数<code>__decorate</code>返回的,那么问题来了:<strong><code>__decorate</code>函数在哪声明的呢?</strong></p>
<p>掘地三尺不难找到,来一起把玩一下:</p>
<pre><code>var __decorate = this.__decorate || function (decorators, target, key, desc) {
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
return Reflect.decorate(decorators, target, key, desc);
}
switch (arguments.length) {
case 2:
return decorators.reduceRight(function(o, d) {
return (d && d(o)) || o;
}, target);
case 3:
return decorators.reduceRight(function(o, d) {
return (d && d(target, key)), void 0;
}, void 0);
case 4:
return decorators.reduceRight(function(o, d) {
return (d && d(target, key, o)) || o;
}, desc);
}
};</code></pre>
<p>第一行使用了或操作符(<code>||</code>),以确保如果函数<code>__decorate</code>已被创建,他将不会被重写。</p>
<pre><code>if (typeof Reflect === "object" && typeof Reflect.decorate === "function")</code></pre>
<p>第二行是一个条件语句,使用了JavaScript的一个新特性:<strong>元数据反射</strong>。这个主题后续再展开讲述,下面我们先聚焦观察下该新特性的兼容方案:</p>
<pre><code>switch (arguments.length) {
case 2:
return decorators.reduceRight(function(o, d) {
return (d && d(o)) || o;
}, target);
case 3:
return decorators.reduceRight(function(o, d) {
return (d && d(target, key)), void 0;
}, void 0);
case 4:
return decorators.reduceRight(function(o, d) {
return (d && d(target, key, o)) || o;
}, desc);
}</code></pre>
<p>此处<code>__decorate</code>函数接受了4个参数,所以<code>case 4</code>将被执行。平心而论这块代码有点生涩,没关系掰开揉碎了看。</p>
<blockquote>
<code>reduceRight</code>方法接受一个函数作为累加器和数组的每个值(从右到左)将其减少为单个值。</blockquote>
<p>为了方便理解,上面的代码重写如下:</p>
<pre><code>[log].reduceRight(function(log, desc) {
if(log) {
return log(C.prototype, "foo", desc);
}
else {
return desc;
}
}, Object.getOwnPropertyDescriptor(C.prototype, "foo"));</code></pre>
<p>可以看到当这段代码执行的时候,装饰器函数<code>log</code>被调用,并且参数<code>C.prototype</code>,<code>"foo"</code>,<code>previousValue</code>也被传入,如此之前的问题现在可以解答了。<br>经过装饰过的<code>foo</code>方法,它依然按照原来的方式执行,只是额外执行了附件的装饰器函数<code>log</code>的功能。</p>
<pre><code>const c = new C();
const r = c.foo(23); // "Call: foo(23) => 46"
console.log(r); // 46</code></pre>
浏览器自动化操作标准--WebDriver
https://segmentfault.com/a/1190000017841619
2019-01-10T13:58:02+08:00
2019-01-10T13:58:02+08:00
felix
https://segmentfault.com/u/felix_5b3b2d56f1a56
3
<p>WebDriver是一个浏览器远程控制协议,是一个既定标准,它本身的内容非常丰富,本文不可能全部介绍,本文仅粗略带大家了解一下WebDriver的部分内容以及一个小的实际应用。想深入了解的请参考W3C文档<a href="https://link.segmentfault.com/?enc=VwbbaMkUSJjSdYuuT5COGw%3D%3D.09i8kFn2B95gkJlx4phEEKiOtxlJwdsmOLTyznoG8fVx9jP8%2Bu82uEvB%2Bb5amkwT" rel="nofollow">WebDriver</a>.</p>
<h2>问题背景</h2>
<p>开发的同学都知道公司为了便于开发和测试都会有多套环境,比如dev开发环境、qa测试联调环境、pre预发模拟线上环境、online线上环境。经常切环境也是一个比较繁琐的事情,简单来说,作为一个前端开发,频率最高的操作路径是:</p>
<p>1.通过SwitchHosts切换host</p>
<p><img src="/img/bVbmYgs?w=1600&h=1000" alt="clipboard.png" title="clipboard.png"></p>
<p>2.选择目录和网关(由于环境不够用,但是又需要部署多套代码。为了解决这个问题,我们利用nginx来在一台机器上配置多个目录,每个目录对应不同的代码,当你访问这个页面的时候,网关会记住你所选择的目录,进而对你的请求进行相应的转发)</p>
<p><img src="/img/bVbmYhy?w=1882&h=812" alt="clipboard.png" title="clipboard.png"></p>
<p>3.登录网页进行ui调试和接口联调</p>
<p>SwitchHosts切换host还算方便,但是选择目录、选择网关、打开网页输入用户名密码然后点登录,这个过程操作频率比较高,有点繁琐。能不能自动化这个过程呢?熟悉自动化测试的同学对这个就非常了解了,端到端测试就是利用自动化测试套件模拟用户访问网页的过程。这里我采用selenium-webdriver这个库,通过node来执行自动化脚本,代码如下。<br><img src="/img/bVbm03w?w=1674&h=1092" alt="图片描述" title="图片描述"></p>
<p>那么selenium-webdriver到底是如何与浏览器进行交互的?如何与不同的浏览器进行交互呢?</p>
<p>下面就要引入主角了—— WebDriver, WebDriver是W3C的一个标准,它是一个标准,所以不同的浏览器都会有自己的实现,而selenium-webdriver是通过WebDriver协议与浏览器进行交互的。</p>
<h2>WebDriver是什么</h2>
<p>WebDriver是W3C的一个标准,是一个远程控制协议,它提供了跨平台和跨语言的方式来远程操控浏览器,它提供了一系列接口来访问和操作DOM,进而控制浏览器的行为。它使得web开发者能写一些自动化脚本来测试网页。</p>
<h2>WebDriver的工作过程</h2>
<p>(主要参考:<a href="https://link.segmentfault.com/?enc=OEQYto53clgCRqC9SGG5YQ%3D%3D.JfncPdVjnXm2MTLrlllIxciV5vxmUE17CzIGlSspts754R9JGWmPbbSJV3%2FFc01lCGnLTFs98wxrVDjZxb0FEA%3D%3D" rel="nofollow">https://blog.csdn.net/ant_ren...</a>)<br>浏览器在启动后会在某一个端口启动基于WebDriver协议的Web Service,接下来我们调用WebDriver的任何api时,都需要借助一个ComandExecutor发送一个命令(也就是给监听端口上的Web Service发送一个http请求),这个命令会告诉浏览器接下来要做什么。</p>
<p><img src="/img/bVbm1nq?w=1314&h=516" alt="clipboard.png" title="clipboard.png"></p>
<h2>WebDriver的实际应用</h2>
<h3>selenium-webdriver</h3>
<p>这是一个浏览器自动化库,它提供了许多浏览器自动化接口,用于测试web应用。<br>除了通过npm安装selenium-webdriver之外,还需要安装浏览器相应的驱动。<br>它相应的api和用法<a href="https://link.segmentfault.com/?enc=8pb%2FpDPHA%2BN%2FWF3ZiYCl%2Fg%3D%3D.KjGHAXubvTMEzIf894KMOF1cVzz6%2F05A7eccfT23uzXpk4agP7OIrxUq4OAwAxPyIbAhT6cieEgDx6NJ3YSEVA%3D%3D" rel="nofollow">selenium-webdriver</a></p>
<p>在我们new一个WebDriver的过程中,selenium首先会确认浏览器的native component是否存在可用而且匹配的版本,然后就在目标浏览器里启动一整套Web Service,这套Web Service使用了selenium自己设计定义的协议,名字叫做The WebDriver Wire Protocol。这套协议非常之强大,几乎可以操作浏览器做任何事情,包括打开、关闭、最大化、最小化、元素定位、元素点击、上传文件等等。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=AiwCQArn4QZOCnBlZN5CJg%3D%3D.hppj6RR43qzvBJ4nB2Rm0NTTTOUf04uNOfVKQUNjp381PqhpBR5IpYmYgT2klyDA%2Frx63ArsgVXeDi%2FsaXXXng%3D%3D" rel="nofollow">https://www.cnblogs.com/linbo...</a><br><a href="https://link.segmentfault.com/?enc=o4%2FKUEwHOq5bAsZ1VVOCXg%3D%3D.wyIecTrvxaj5rfutk3z8%2F7RIi4%2FvkYNs4DaVnfPxkoaLMEgUXkjpI%2B3qktFdRuBO" rel="nofollow">https://cloud.tencent.com/dev...</a><br><a href="https://link.segmentfault.com/?enc=wrZOKe%2ByEf9X2%2BDgy7IpHA%3D%3D.Al2Sv4kIh%2BVYsn0HVt8bgYdubTbVoZ1vg%2BFa2vX78%2FP9Kgp79iZZDa886eSiMok7hbQXn49o%2FNo2UWWGChEzvGavQDVJx42gPMP0oe7uQkA%3D" rel="nofollow">http://www.cnblogs.com/sunada...</a></p>
深入理解Flex布局 -- flex-grow & flex-shrink & flex-basis
https://segmentfault.com/a/1190000017826957
2019-01-09T13:08:15+08:00
2019-01-09T13:08:15+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
40
<p>欢迎关注我的公众号<code>睿Talk</code>,获取我最新的文章:<br><img src="https://segmentfault.com/img/bVbmYjo" alt="clipboard.png" title="clipboard.png"></p>
<h3>一、前言</h3>
<p>最近在项目里遇到了一个 Flex 布局的问题,才发现自己对它的理解还是停留在浅显的水平,遇到一些特殊情况就不知道如何处理。于是找了些资料深入学习一下,然后将我的学习心得总结成这篇文章。</p>
<h3>二、问题还原</h3>
<p>先讲讲我遇到的问题。我希望实现一个左中右三列的布局,其中左右部分固定宽度,中间部分自适应:<br><img src="https://segmentfault.com/img/bVbmWPD" alt="clipboard.png" title="clipboard.png"></p>
<p>实现起来很简单,代码如下:</p>
<pre><code><div class="container">
<div class="left">left</div>
<div class="middle">
middle
</div>
<div class="right">right</div>
</div>
.container {
display: flex;
width: auto;
height: 300px;
background: grey;
}
.left {
flex-basis: 200px;
background: linear-gradient(to bottom right, green, white);
}
.middle {
flex: 1;
background: linear-gradient(to bottom right, yellow, white);
}
.right {
flex-basis: 300px;
background: linear-gradient(to bottom right, purple, white);
}</code></pre>
<p>到此为止一切都很美好。但遇到中间部分内容很长的时候,UI 就变形了:<br><img src="https://segmentfault.com/img/bVbmWT0" alt="clipboard.png" title="clipboard.png"></p>
<p>为了固定住左右部分的宽度,需要给 left 和 right 加上<code>flex-shrink: 0</code>。但加上后容器的宽度就被撑开了,页面底部出现了滚动条:<br><img src="https://segmentfault.com/img/bVbmWWK" alt="clipboard.png" title="clipboard.png"></p>
<p>而我期望的效果是滚动条出现在中间部分,整个页面不能滚动。解决方法是给 middle 加上<code>overflow: scroll</code>:<br><img src="https://segmentfault.com/img/bVbmWXM" alt="clipboard.png" title="clipboard.png"></p>
<p>此时的完整代码如下:</p>
<pre><code><div class="container">
<div class="left">left</div>
<div class="middle">
middle
<!-- 宽度为800px的内容-->
<div class="long">long</div>
</div>
<div class="right">right</div>
</div>
.container {
display: flex;
width: auto;
height: 300px;
background: grey;
}
.left {
flex-basis: 200px;
flex-shrink: 0;
background: linear-gradient(to bottom right, green, white);
}
.middle {
flex: 1;
overflow: scroll;
background: linear-gradient(to bottom right, yellow, white);
}
.right {
flex-basis: 300px;
flex-shrink: 0;
background: linear-gradient(to bottom right, purple, white);
}
.long {
width: 800px;
}</code></pre>
<p>完整的 <code>codepen</code> 在<a href="https://codepen.io/dickenslian/pen/QzxBoV">这里</a></p>
<p>实战经验到此结束,下面我们再深入学习涉及到的知识点。</p>
<h3>三、知识点</h3>
<p>先来讲讲上面用到的属性<code>flex: 1</code>。它其实是一个缩写,等价于<code>flex: 1 1 0</code>,也就是</p>
<pre><code class="css">flex-grow : 1;
flex-shrink : 1;
flex-basis : 0;</code></pre>
<ul>
<li>flex-grow 表示当有剩余空间的时候,分配给项目的比例</li>
<li>flex-shrink 表示空间不足的时候,项目缩小的比例</li>
<li>flex-basis 表示分配空间之前,项目占据主轴的空间</li>
</ul>
<p>下面来讲讲 flex 空间分配的步骤。</p>
<ul><li><h3>flex-grow(默认值 0)</h3></li></ul>
<p>假设有一个宽度为 800 的容器,里面有 3 个项目,宽度分别是 100,200,300:</p>
<pre><code><div class="container">
<div class="left">left</div>
<div class="middle">middle</div>
<div class="right">right</div>
</div>
.container {
display: flex;
width: 800px;
height: 300px;
background: grey;
}
.left {
flex-basis: 100px;
background: linear-gradient(to bottom right, green, white);
}
.middle {
flex-basis: 200px;
background: linear-gradient(to bottom right, yellow, white);
}
.right {
flex-basis: 300px;
background: linear-gradient(to bottom right, purple, white);
}</code></pre>
<p>效果如下:<br><img src="https://segmentfault.com/img/bVbmXeh" alt="clipboard.png" title="clipboard.png"></p>
<p>这时候就出现了多余的 200 的空间(灰色部分)。这时候如果我们对左中右分别设置<code>flex-grow</code>为 2,1,1,各个项目的计算逻辑如下:</p>
<ol>
<li>首先将多余空间 200 除以 4(2 + 1 + 1),等于 50</li>
<li>left = 100 + 2 x 50 = 200</li>
<li>middle = 200 + 1 x 50 = 250</li>
<li>right = 300 + 1 x 50 = 350</li>
</ol>
<p><img src="https://segmentfault.com/img/bVbmXi4" alt="clipboard.png" title="clipboard.png"></p>
<ul><li><h3>flex-shrink(默认值 1)</h3></li></ul>
<p>假设父容器宽度调整为 550,里面依然是 3 个项目,宽度分别是 100,200,300,这时候空间就不够用溢出了。首先要理解清楚,当我们定义一个固定宽度容器为<code>flex</code>的时候,<code>flex</code>会尽其所能不去改变容器的宽度,而是压缩项目的宽度。这时我们对左中右分别设置<code>flex-shrink</code>为 1,2,3,计算逻辑如下:</p>
<ol>
<li>溢出空间 = 100 + 200 + 300 - 550 = 50</li>
<li>总权重 = 1 x 100 + 2 x 200 + 3 x 300 = 1400</li>
<li>left = 100 - (50 x 1 x 100 / 1400) = 96.42</li>
<li>middle = 200 - (50 x 2 x 200 / 1400) = 185.72</li>
<li>right = 300 - (50 x 3 x 300 / 1400) = 267.86</li>
</ol>
<p><img src="https://segmentfault.com/img/bVbmXIA" alt="clipboard.png" title="clipboard.png"></p>
<p>如果我们不想项目被压缩,就必须将<code>flex-shrink</code>设为 0。还是用上面的例子,当左中右的<code>flex-shrink</code>都为 0 的时候,就会冲破宽度限制,container的宽度将会从 550 变为 600。<br><img src="https://segmentfault.com/img/bVbmXJP" alt="clipboard.png" title="clipboard.png"></p>
<p><code>codepen</code> 在<a href="https://codepen.io/dickenslian/pen/zyLwKz">这里</a></p>
<ul><li><h3>flex-basis(默认值 auto)</h3></li></ul>
<p><code>flex-basis</code>指定项目占据主轴的空间,如果不设置,则等于内容本身的空间:<br><img src="https://segmentfault.com/img/bVbmXLd" alt="clipboard.png" title="clipboard.png"></p>
<h3>四、总结</h3>
<p>本文从问题出发,讲解了<code>Flex</code>布局在实战中的应用,并深入到<code>flex-grow</code>,<code>flex-shrink</code>和<code>flex-basis</code>的细节,描述了项目空间在填充和溢出情况下的计算方式,希望对你有所帮助。</p>
React Fiber知识点学习笔记
https://segmentfault.com/a/1190000017784309
2019-01-06T21:11:36+08:00
2019-01-06T21:11:36+08:00
一番
https://segmentfault.com/u/yifan_5b176f35bd3be
9
<p>本文是对React Fiber知识点的学习记录,网上有很多大佬对React Fiber的详细讲解,想要深入了解React Fiber的可以查看文章后面的引用。</p>
<h2>一、React Fiber是什么,解决了什么问题</h2>
<p>React Fiber在React v16引入,相当于是对核心渲染机制的一次重构。在没有引入这种算法之前,React在所有更新都没有完成时会一直占用主线程,直接导致的现象是渲染期间页面的其他js动效会卡住直到主线程继续,使页面出现卡顿的现象。<br>看一个<a href="https://link.segmentfault.com/?enc=CWyR%2Fqwbssh5bUIACu8qEA%3D%3D.JovqsxEh%2FTve27nfN8%2BaThjr%2FScYVSr7BdZHvr%2Bv19%2FAF6BuVFSYPXBjtXhKj4VXXaqgCzJePwlB72ebQIWQMXUeOEJalY4NFPztdjBz0fgMxzq3zvA%2B85KjDg6Vlz1%2B" rel="nofollow">官方例子</a>:</p>
<pre><code class="js"><!DOCTYPE html>
<html style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8">
<title>Fiber Example</title>
</head>
<body>
<h1>Fiber Example</h1>
<div id="container">
<p>
To install React, follow the instructions on
<a href="https://github.com/facebook/react/">GitHub</a>.
</p>
<p>
If you can see this, React is <strong>not</strong> working right.
If you checked out the source from GitHub make sure to run <code>npm run build</code>.
</p>
</div>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
<script type="text/babel">
var dotStyle = {
position: 'absolute',
background: '#61dafb',
font: 'normal 15px sans-serif',
textAlign: 'center',
cursor: 'pointer',
};
var containerStyle = {
position: 'absolute',
transformOrigin: '0 0',
left: '50%',
top: '50%',
width: '10px',
height: '10px',
background: '#eee',
};
var targetSize = 25;
class Dot extends React.Component {
constructor() {
super();
this.state = { hover: false };
}
enter() {
this.setState({
hover: true
});
}
leave() {
this.setState({
hover: false
});
}
render() {
var props = this.props;
var s = props.size * 1.3;
var style = {
...dotStyle,
width: s + 'px',
height: s + 'px',
left: (props.x) + 'px',
top: (props.y) + 'px',
borderRadius: (s / 2) + 'px',
lineHeight: (s) + 'px',
background: this.state.hover ? '#ff0' : dotStyle.background
};
return (
<div style={style} onMouseEnter={() => this.enter()} onMouseLeave={() => this.leave()}>
{this.state.hover ? '*' + props.text + '*' : props.text}
</div>
);
}
}
class SierpinskiTriangle extends React.Component {
shouldComponentUpdate(nextProps) {
var o = this.props;
var n = nextProps;
return !(
o.x === n.x &&
o.y === n.y &&
o.s === n.s &&
o.children === n.children
);
}
render() {
let {x, y, s, children} = this.props;
if (s <= targetSize) {
return (
<Dot
x={x - (targetSize / 2)}
y={y - (targetSize / 2)}
size={targetSize}
text={children}
/>
);
return r;
}
var newSize = s / 2;
var slowDown = true;
if (slowDown) {
var e = performance.now() + 0.8;
while (performance.now() < e) {
// Artificially long execution time.
}
}
s /= 2;
return [
<SierpinskiTriangle x={x} y={y - (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x - s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
<SierpinskiTriangle x={x + s} y={y + (s / 2)} s={s}>
{children}
</SierpinskiTriangle>,
];
}
}
class ExampleApplication extends React.Component {
constructor() {
super();
this.state = {
seconds: 0,
useTimeSlicing: true,
};
this.tick = this.tick.bind(this);
this.onTimeSlicingChange = this.onTimeSlicingChange.bind(this);
}
componentDidMount() {
this.intervalID = setInterval(this.tick, 1000);
}
tick() {
if (this.state.useTimeSlicing) {
// Update is time-sliced.
// 使用时间分片的方式更新
// https://github.com/facebook/react/pull/13488 将此api移除
// deferredUpdates是将更新推迟 https://juejin.im/entry/59c4885f6fb9a00a4456015d
ReactDOM.unstable_deferredUpdates(() => {
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }));
});
} else {
// Update is not time-sliced. Causes demo to stutter.
// 更新没有做时间分片,导致卡顿 stutter(结巴)
this.setState(state => ({ seconds: (state.seconds % 10) + 1 }));
}
}
onTimeSlicingChange(value) {
this.setState(() => ({ useTimeSlicing: value }));
}
componentWillUnmount() {
clearInterval(this.intervalID);
}
render() {
const seconds = this.state.seconds;
const elapsed = this.props.elapsed;
const t = (elapsed / 1000) % 10;
const scale = 1 + (t > 5 ? 10 - t : t) / 10;
const transform = 'scaleX(' + (scale / 2.1) + ') scaleY(0.7) translateZ(0.1px)';
return (
<div>
<div>
<h3>Time-slicing</h3>
<p>Toggle this and observe the effect</p>
<Toggle
onLabel="On"
offLabel="Off"
onChange={this.onTimeSlicingChange}
value={this.state.useTimeSlicing}
/>
</div>
<div style={{ ...containerStyle, transform }}>
<div>
<SierpinskiTriangle x={0} y={0} s={1000}>
{this.state.seconds}
</SierpinskiTriangle>
</div>
</div>
</div>
);
}
}
class Toggle extends React.Component {
constructor(props) {
super();
this.onChange = this.onChange.bind(this);
}
onChange(event) {
this.props.onChange(event.target.value === 'on');
}
render() {
const value = this.props.value;
return (
<label onChange={this.onChange}>
<label>
{this.props.onLabel}
<input type="radio" name="value" value="on" checked={value} />
</label>
<label>
{this.props.offLabel}
<input type="radio" name="value" value="off" checked={!value} />
</label>
</label>
);
}
}
var start = new Date().getTime();
function update() {
ReactDOM.render(
<ExampleApplication elapsed={new Date().getTime() - start} />,
document.getElementById('container')
);
requestAnimationFrame(update);
}
requestAnimationFrame(update);
</script>
</body>
</html></code></pre>
<p>react fiber针对这种情况做了什么优化呢?主要是两点:1、将任务拆分成一小块一小块,2、获取到时间片才执行任务<br>下面主要来讲讲这两点</p>
<h2>二、React Fiber的任务拆分</h2>
<p>学过React的都知道,React有虚拟dom树,vDom-tree,要把计算任务拆分,那就要有任务恢复和任务完成后提交等功能,普通的vDom并不具备记录这些信息的能力,因此React Fiber从虚拟节点衍生出了一套Fiber节点,来记录任务状态。每一个Fiber节点都会对应一个虚拟节点,这样计算任务就拆分成了一个个小块,类似于下图,会有一个对应关系<br><img src="/img/remote/1460000017784312" alt="" title=""><br><img src="/img/remote/1460000017784313" alt="" title=""><br>简单描述下它的流程:<br>1.数据状态发生改变,即发生setState<br>2.开始diff算法<br>3.到达一个节点,生成对应的fiber节点,记录状态<br>4.查看当前是否有执行时间<br>5.有执行时间,计算完当前节点(节点的增删改),到下一个节点,继续这个步骤<br>6.到某个节点没有执行时间,则保存fiber状态,每个fiber都记录了下一个任务指向<br>7.重新获取到了执行时间,从当前记录的fiber节点开始往下继续<br>8.执行到最后的节点,将结果进行一级一级提交,这样直到根节点,组件就知道它已经完成了数据计算<br>9.最后一步,将最终确认的dom结果渲染到页面中</p>
<h2>三、如何获取到时间片的</h2>
<p>这个就比较简单了,刚开始我还以为React使用了什么骚操作来弄得,看了大佬们的文章后才知道,原来是用了浏览器的api: <a href="https://link.segmentfault.com/?enc=hqLm826pwzN38jrs%2Bt5CbA%3D%3D.TK2i4VkuobPmHMQsD47Hd3kwCKW9xvx3bnXs152Ek0jvsu1Ci9DkVjXsIMEeQ%2FpPDEmJu4iXUSSCXze74LjY0JH6UeptLIFOvUzdmTuXGFc%3D" rel="nofollow">requestIdleCallback</a>和<a href="https://link.segmentfault.com/?enc=38stNgvOYxpVNLh8T94Meg%3D%3D.hFkQlE8pCUr23%2FC%2B3OHLjtMEWLBVsIyKAlBn2kAI4K7yUcPdo9sGWx%2BwTesntlqVkTTv3a7kFnu8w7qEQViKsagpZQGixLtfriXJJLaSG2w%3D" rel="nofollow">requestAnimationFrame</a>,requestAnimationFrame会在每一帧结束后确定调用它的回调函数,用于处理高优先级任务,为了达到不影响页面效果requestIdleCallback用于处理低优先任务,它的执行时机不确定。主要说下requestIdleCallback,每次调用requestIdleCallback,会告诉你现在是否是空闲时间,空闲时间有多久,它的用法:</p>
<pre><code class="js">// 一窥它的行为
requestIdleCallback(function(){
console.log('1');
let a = 1000000000;
while(a > 0){ a--; }
})
requestIdleCallback(function(){console.log('2')})
requestIdleCallback(function(){console.log('3')}, {timeout:10})
// 使用方式
const tasks = [...] // 任务队列
function taskHandler(deadline) {
// deadline.timeRemaining() 可以获取到当前帧剩余时间
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
// do something
}
// 没时间了,但还有任务,继续注册一个,等到下次有时间了会执行
if (tasks.length > 0){
requestIdleCallback(taskHandler);
}
}
requestIdelCallback(taskHandler);</code></pre>
<p>上面说到任务的优先级,在fiber任务执行完进行dom更新的时候,这块是没法做任务拆分的,如果遇到dom变化很大,更新耗时的情况也会造成卡顿,这个没法避免,如果此时有用户交互发生,造成卡顿会降低用户体验,Fiber针对这种情况也做了优化,将任务分成优先级,像用户输入,交互等行为属于高优先级,会优先处理,然后页面渲染,diff计算等属于次优先级。</p>
<p>几篇对React Fiber不错的介绍:<br><a href="https://link.segmentfault.com/?enc=YKHDIhp2UUSGyeCwVdptBg%3D%3D.pZDNbxiKMGMba2SVbXfhWTkaG%2FPQlOGforksoTzxKzx8Wm8LHrwWLWSRDZ1sNULNvh0GF0bnGsDiR4%2BXQQoP2A%3D%3D" rel="nofollow">React-从源码分析React Fiber工作原理</a><br><a href="https://link.segmentfault.com/?enc=nvoDxywf9HC1MGzx2EWWmQ%3D%3D.FeB%2Bv7G8iBLKuWTrbQ%2FLMITccR4dVc9WO84IwUHl5EyvWGaKMgfjorH5k5sM2mBi0wcEntKi29YYHD0T%2Bcelsg%3D%3D" rel="nofollow">理解react16.3的fiber架构</a><br><a href="https://link.segmentfault.com/?enc=PSJRIQlzzsdHB3RYx5bnzA%3D%3D.KlOWzrbMoAHEfHbmII87WYV%2Bk4Wl0gT9gdQWdk5W3DO3zSW6GfFxnevxHB%2FcprLO" rel="nofollow">React Fiber</a><br><a href="https://link.segmentfault.com/?enc=hvmgso9ukzVOVYecaqB18w%3D%3D.EtEIv6Y%2BacMbYU6dz1FWKJplrYLPYXutyL%2Fv%2FjTKv9o0kLiNBudR5WcTS4TUdKEQ" rel="nofollow">React Fiber是什么</a><br><a href="https://link.segmentfault.com/?enc=SVxOiL2Uv6igPAkVU5%2FyMg%3D%3D.xCs8EuQAvVVKp7BmEifHttG6bm%2FHIVuqQO147AdET5kJOjiT7Fgrmjf9jLvNlaCjpSjWVdCfQejnKlUZkp1Gyg%3D%3D" rel="nofollow">React Fiber初探</a><br><a href="https://link.segmentfault.com/?enc=sOFkWlaLlCLRPIp6eXEDpg%3D%3D.zAjT0X%2BSJMkOAG5x28WMIwULYOPjISH4Nn5mJiiNlUaUSo8MHxuZvLEYwcx4jiUj" rel="nofollow">react-fiber-resources</a></p>
VS Code插件开发介绍(二)
https://segmentfault.com/a/1190000017541563
2018-12-27T19:08:21+08:00
2018-12-27T19:08:21+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
3
<ul><li><h3>一、前言</h3></li></ul>
<p>在<a href="https://segmentfault.com/a/1190000016641617">上一篇文章</a>里,我简要介绍了 VSCode 插件开发的基本流程,同时讲解了如何获取文件夹绝对路径和用户输入的方法。最近又开发了一个新的插件,主要用途是替换当前编辑文件的内容。google 了一圈,发现介绍这方面的文章很少,特此记录一下,希望对有类似需求的人有一些帮助。</p>
<ul><li><h3>二、需求</h3></li></ul>
<p>需求很简单,我需要将下面文件的内容:</p>
<pre><code class="javascript">export default {
add_member#
manage_member_card#
member_setting#
search_member#
edit_member#
delete_member#
assign_consultant#
add_member_tag#
import_member#
modify_member_point#
};</code></pre>
<p>替换为:</p>
<pre><code class="javascript">export default {
add_member: 'ce0',
manage_member_card: 'ce1',
member_setting: 'ce2',
search_member: 'ce3',
edit_member: 'ce4',
delete_member: 'ce5',
assign_consultant: 'ce6',
add_member_tag: 'ce7',
import_member: 'ce8',
modify_member_point: 'ce9',
};</code></pre>
<p>可以理解为一个简单的自动化编号工具。其中要解决的问题主要有下面三个:</p>
<ul>
<li>获取当前文件路径</li>
<li>读取文件内容</li>
<li>写文件内容</li>
</ul>
<p>下面介绍如何实现。</p>
<ul><li><h3>三、实现</h3></li></ul>
<p>开始以为 VSCode 有现成的 API 可以取到当前文件内容,但找了一圈搜不到,只能通过迂回的方式实现。</p>
<p>第一步,获取当前文件的路径:</p>
<pre><code class="javascript">const currentlyOpenTabfilePath = vscode.window.activeTextEditor.document.fileName;</code></pre>
<p>第二步,读取文件内容,并拆分为数组</p>
<pre><code class="javascript">const fs = require('fs');
const fileContentArr = fs.readFileSync(currentlyOpenTabfilePath, 'utf8').split(/\r?\n/);</code></pre>
<p>第三步,写文件。由于没法逐行替换文件内容,只能现将原来的文件清空,再一行一行添加回去。</p>
<pre><code class="javascript">fs.truncateSync(currentlyOpenTabfilePath);
fileContentArr.forEach( (line, index) => {
let content = line;
if (line.slice(-1) == '#') {
content = xxxxx;
}
fs.appendFileSync(currentlyOpenTabfilePath, content + ((index == contentLength - 1) ? '' : '\n'));
})</code></pre>
<ul><li><h3>四、总结</h3></li></ul>
<p>其实这个需求实现起来还是蛮简单的,主要是要根据 VSCode 的特点将思路理顺,再一步步实现。如果有更好的实现方式,请务必留言给我<span class="emoji emoji-bow"></span></p>
React 16 升级总结
https://segmentfault.com/a/1190000017540511
2018-12-27T17:38:07+08:00
2018-12-27T17:38:07+08:00
Dickens
https://segmentfault.com/u/dabai_5955b2921e87d
17
<p>欢迎关注我的公众号<code>睿Talk</code>,获取我最新的文章:<br><img src="https://segmentfault.com/img/bVbmYjo" alt="clipboard.png" title="clipboard.png"></p>
<h3>一、前言</h3>
<p>目前 React 最新的版本是 16.7.0,基于全新的 React Fiber 架构,有众多激动人心的新功能。由于是大版本升级,考虑到业务的稳定性,我们团队大概等了一年的时间,终于鼓起勇气着手升级的事情,特以此文来记录升级过程中遇到的坑。</p>
<h3>二、升级的好处</h3>
<p>这次升级的目标是将 React 从版本 15.6.2 升级到 16.2.0。原因是 16.2.0 为止引入了几个不错的新特性,同时对现有代码的影响会相对较小,风险可控。比较吸引我的三个新特性如下:</p>
<ul><li>文件大小减少30%。官网原文如下:</li></ul>
<blockquote>react + react-dom is 109 kb (34.8 kb gzipped), down from 161.7 kb (49.8 kb gzipped).</blockquote>
<ul><li>
<a href="https://link.segmentfault.com/?enc=NWYiK4UM0TUz%2FlGT7CyTyg%3D%3D.pIQfSnxFVJNeJuPp5NUOUbawo6eSKcqBkn0OY11%2F%2FcD%2BMY5ClMNIDvxjupyVB9feVKeYbsXGXW7BRY%2FGCd25nyoqFAWK02hId5XG8Ub%2FT5w%3D" rel="nofollow">Error Boundaries</a>,可以将错误限制在可控范围,出错时组件可以展示应对错误的 UI。</li></ul>
<pre><code class="javascript">class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
</code></pre>
<ul><li>
<a href="https://link.segmentfault.com/?enc=jztfm2VE4QPsqyD3d03C5g%3D%3D.k1q3rKVsoXwFdrCJ7xZfNnrx8jwgInZEF%2Bz4IN0NhB7VqY4ucR6LC9Am0voMqIpR0VUwc6Ikq%2B3sIPGN1q%2FTM8DKoLBtHyHLT81FJyy85N0%3D" rel="nofollow">Fragments</a>,可以在 render 返回多个一级组件,而不需要在外面包一个<code>div</code>
</li></ul>
<pre><code class="javascript">const Fragment = React.Fragment;
<Fragment>
<ChildA />
<ChildB />
<ChildC />
</Fragment></code></pre>
<h3>三、升级遇到的问题</h3>
<p>1、依赖<br>项目中用到了 React Router 3 和 Redux。原计划将 Router 升级到 v4,但改动实在太大,放弃了,只是升级到支持 16 的版本。其它升级的依赖如下:<br><img src="/img/bVblK4R?w=742&h=174" alt="clipboard.png" title="clipboard.png"></p>
<p>2、React.PropTypes<br>这种写法已经不支持了,要改成:</p>
<pre><code class="javascript">// import { PropTypes } from 'React' 已废弃
import PropTypes from 'prop-types';</code></pre>
<p>3、ReactDOM.render<br>在生命周期函数里面,<code>ReactDOM.render</code>会返回<code>null</code>,因此类似下面这样的代码就会报错</p>
<pre><code class="javascript">function newInstance(props) {
...
let loading = ReactDOM.render(<Loading {...props} />, div);
return {
show: loading.show, // Error, loading 为 null
container: div,
};
}</code></pre>
<p>4、setState(null) 不触发 render<br>如果需要强制刷新的话,可以使用<code>this.forceUpdate()</code></p>
<p>上述的问题主要是 16.0.0 带来的问题,更详尽的升级指南可以看<a href="https://link.segmentfault.com/?enc=tIYFocUowMAx4ho6badFyA%3D%3D.zVCxLFWG62AovxF4rlWflWQHHSDtJ9RIns5c4NO1KCPjJ8NnZJJ7xJIvmrfId8nafW9MTLa2dO5NsDUyXZPYzWMO8ovqsqQoPGZRgQ8BmeI%3D" rel="nofollow">这里</a>的。</p>
<h3>四、总结</h3>
<p>总体来说,升级没有遇到特别大的困难,主要就是针对官方文档的<a href="https://link.segmentfault.com/?enc=JiMYXozH6VV5LP9YIRWkqg%3D%3D.lqyoijyKP8JS709hBdkL9IPYxcuq5ZehtdxaNOlfhzy3mPRBrDcjEFYqUMtqoZm8dX181L6K6kkF1DhAhADOrP9NbDF92zXPWsdjFw7Pydk%3D" rel="nofollow">Breaking changes</a>部分,进行全局搜索,然后进行修改。另外,还有可能依赖的库用到了已经不支持的 API,例如<code>PropTypes</code>,应对办法就是升级对应的库。</p>
<p>先聊到这里,有其它疑问,欢迎留言交流。</p>
node核心模块-Buffer
https://segmentfault.com/a/1190000017442398
2018-12-19T20:04:20+08:00
2018-12-19T20:04:20+08:00
petruslaw
https://segmentfault.com/u/petruslaw
0
<h3>什么是buffer</h3>
<p>Buffer 类是一个全局变量。Buffer 类的实例类似于整数数组,但 Buffer 的大小是固定的、且不与 V8 共用内存。 Buffer 的大小在创建时确定,且无法改变。</p>
<h3>Buffer.form</h3>
<pre><code class="javascript">console.log(Buffer.from('hello world'));
// <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
console.log(Buffer.from([65, 66, 67, 68]));
// <Buffer 41 42 43 44>
const arr = new Uint8Array(2);
arr[0] = 65;
arr[1] = 66;
const buf = Buffer.from(arr.buffer);
console.log(buf.toString('utf8'));
// AB
arr[1] = 67;
console.log(buf.toString('utf8'));
// AC</code></pre>
<p>使用字符/数组/数组/arrayBuffer 创建buffer。<br>如果使用 arrayBuffer 创建buffer,arrayBuffer 与 Buffer内存将共享。<br><a href="https://link.segmentfault.com/?enc=P7oAsMsvSX6r7%2FtZRXsZlg%3D%3D.IYkxJo53tuKFlVEi2M%2BdJuVMHfkLg0nQI2v59ijrSKz1a8HGjGU1KhtDbIRGR9l3PrP%2F6DxfUZ5hxO7rW9T%2BhJjpkftJDFLZ3NbSWm%2FEQeo1PIlekA0xPwema7i8AyFN" rel="nofollow">ArrayBuffer传送门</a></p>
<h3>Buffer.alloc</h3>
<pre><code class="javascript">console.log(Buffer.alloc(5));
console.log(Buffer.alloc(5, 'abc').toString());
console.log(Buffer.alloc(5, 'abcdef').toString('utf8'));
console.log(Buffer.alloc(5, '见面打声招呼', 'gb2312').toString('utf8'));
// <Buffer 00 00 00 00 00>
// abcab
// abcde
// 见�</code></pre>
<p>填充内容不足的情况下重复填充。<br>填充内容大小超出可用内存大小将被截断;<br>中文按照三个字节来计算,所以上面出现了乱码;<br>不传入填充内容的情况下使用空字符填充Buffer,这里的空字符不是指空格字符;</p>
<h3>Buffer.allocUnsafe</h3>
<pre><code class="javascript">console.log(Buffer.allocUnsafe(5));
console.log(Buffer.allocUnsafe(5));
// <Buffer 60 07 04 03 01>
// <Buffer 80 07 04 03 01></code></pre>
<p>等同于 node v6.0.0 之前的 new Buffer();以这种方式创建的 Buffer 的内存是未初始化的。 Buffer 的内容是未知的,可能包含已存在数据。<br>不推荐,如果一定要用,使用需要使用 Buffer.fill 进行填充,或者直接使用Buffer.alloc。<br>Buffer 模块预先分配大小为 8Kb (Buffer.poolSize)的内部 Buffer 池,用来快速分配给新 Buffer 实例。Buffer.alloc 永远不会使用内部 Buffer 池。Buffer 池空间大于一半时,Buffer.allocUnsafe 将优先使用预分配的 Buffer 池,返回一个内存地址,类似于指针概念。</p>
<h3>Buffer. allocUnsafeSlow</h3>
<pre><code class="javascript">Buffer.allocUnsafeSlow();</code></pre>
<p>与 Buffer.allocUnsafe 的区别是,不会使用预分配 Buffer池,而是从外部获取一块内存,生成新的 Buffer。可以避免 Buffer池 创建太多的Buffer。</p>
<h4>Buffer 文件读取</h4>
<pre><code class="javascript">const txtPath = path.join(__dirname, './test.txt');
const content = fs.readFileSync(txtPath);
// <Buffer 61 62 63 ...>
console.log(content.toString('utf8'));
// content</code></pre>
<p>node 中文件的传输与读取以及写入操作都是有基于 Buffer 进行操作。</p>
<h3>其他</h3>
<p>Buffer 还有各种转码,以及读取写入等操作,具体看<a href="https://link.segmentfault.com/?enc=%2FORFnY34W8vGnjKOft3kUw%3D%3D.4g8MCnvIzyfjiDV%2FUIgF2QqSqmj3Tc6cYNb%2Bnj76TB5GZceWxiRMUEugsawc3f50" rel="nofollow">API</a> 这里不做过多介绍</p>
<h3>使用场景</h3>
<p>内容等比切割<br>文件读取传输操作<br>资源临时存储,不如js,css等静态文件<br>...</p>
<h3>参考资料</h3>
<p><a href="https://link.segmentfault.com/?enc=6EUz2YNGfNMgi9iw4jpZDQ%3D%3D.CVvm6mZMzPAB3OeL7BfLRBhL2%2F4kSwwbmLtCu8uG4njg%2FB%2F05ZmHUQTHzBqx3X6e" rel="nofollow">http://nodejs.cn/api/buffer.html</a><br><a href="https://link.segmentfault.com/?enc=OTRob8FRx7kIkvLFAqjAog%3D%3D.W%2FTdhgduV2CUIPjAOKFN2xHLnWOeD71QLX5aVDJIMKgcdA7sHpdSzUhwfN%2FT5N%2FJOfjSN%2BGrLFa61tTuKC36Fuz4aXpGumzH4NwDEa77mjrYCVq29e8ZQhNgRmlvlnE1" rel="nofollow">https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer</a><br><a href="https://link.segmentfault.com/?enc=f%2F%2BVqwWB7imXougTk%2BhMhA%3D%3D.ww4i9JkXJkWc2rxpj62XlsELN%2FizNY8VuXzX0MldrYky92OVYKn46IVwZZ97QVb3Nd88BzV%2FlmLHpaOLYRta3tnkiW1E9wWb3c%2FgNVDPEBY%3D" rel="nofollow">http://guojing.me/linux-kernel-architecture/posts/how-slab-work/</a></p>