手把手教你写一个 Javascript 框架:项目结构

ThorstenGri 8年前
   <p>过去几个月中,RisingStack 的 JavaScript 工程师 Bertalan Miklos 编写了新一代客户端框架 NX 。Bertalan 将通过 <strong>编写 JavaScript 框架</strong> 系列文章与我们分享他在编写框架过程中的收获:</p>    <p>本章将展示 NX 的项目结构,并讲述如何解决可扩展性、依赖注入以及私有变量等方面的一些困难。</p>    <p>本系列章节如下:</p>    <ol>     <li>项目结构(正是本文)</li>     <li>执行调度(Execution timing)</li>     <li>沙箱求值</li>     <li>数据绑定简介</li>     <li>ES6 Proxy 实现数据绑定</li>     <li>自定义元素</li>     <li>客户端路由</li>    </ol>    <h2><strong>项目结构</strong></h2>    <p>没有放之四海而皆准的项目结构,但有一些基本准则。感兴趣的同学可以看下我们的 Node Hero 系列中的《 Node.js 项目结构教程 》这一章。</p>    <h3><strong>NX 框架概览</strong></h3>    <p>NX 的目标是成为一个开源社区驱动的易于扩展的项目。项目特点如下:</p>    <ul>     <li>包含现代客户端框架必须的所有特性;</li>     <li>除 polyfill 外,没有任何外部依赖;</li>     <li>代码总量 3000 行;</li>     <li>没有代码多于 300 行的模块;</li>     <li>单个特性模块依赖不超过 3 个。</li>    </ul>    <p>项目各模块依赖关系如下图所示:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/890ad122621cb3bf8bcadac2dba557f6.png"></p>    <p>这种结构为典型框架开发难题提供了一种解决方案。</p>    <ul>     <li>扩展性</li>     <li>依赖注入</li>     <li>私有变量</li>    </ul>    <h3><strong>可扩展性实现</strong></h3>    <p>社区驱动项目必须易于扩展。故项目的核心部分应当小巧,并拥有一个预定义的依赖处理系统。前者确保项目易于理解,后者则保证框架稳定。</p>    <p>本节先聚焦于实现小巧的内核。</p>    <p>现代框架应当拥有的主要特性就是创建自定义元素并将其应用于 DOM 的能力。NX 的核心只有一个 component 函数,它的工作正在于此这个函数允许用户配置、注册一个新类型的元素。</p>    <pre>  <code class="language-javascript">component(config)    .register('comp-name')</code></pre>    <p>注册的 comp-name 是空组件类型,可以按照预期在 DOM 中实例化。</p>    <pre>  <code class="language-javascript"><comp-name></comp-name></code></pre>    <p>下一步是保证能使用新特性扩展组件。为保持简洁、可扩展,这些新特性不应该污染核心部分。这时候使用依赖注入就很方便了。</p>    <h2><strong>利用中间件实现依赖注入(DI)</strong></h2>    <p>依赖注入是一种设计模式,在这种模式中,一个或多个依赖或服务被注入到或引用传递给一个独立对象。</p>    <p>DI 解决了硬性依赖,却引入了新问题。使用者需要知道如何配置、注入依赖。大多客户端框架都将这些工作交给 DI 容器,帮助开发者完成。</p>    <p>DI 容器指的是知道如何实例化、配置对象的对象。</p>    <p>另外一种方式则是中间件 DI 模式,这在服务端得到广泛应用(如 Express、Koa 等)。其中的奥秘在于,所有可注入的依赖(中间件)拥有相同的接口,以相同方式注入。这种方法则无需 DI 容器。</p>    <p>为保持简洁,我决定采用中间件模式。若你曾使用过 Express,以下代码自然不会陌生:</p>    <pre>  <code class="language-javascript">component()    .use(paint) // inject paint middleware    .use(resize) // inject resize middleware    .register('comp-name')    function paint (elem, state, next) {    // elem is the component instance, set it up or extend it here    elem.style.color = 'red'    // then call next to run the next middleware (resize)    next()  }    function resize (elem, state, next) {    elem.style.width = '100 px'    next()  }</code></pre>    <p>中间件在新的组件实例插入 DOM 时执行,通常会给实例扩展一些新特性。如若不同库扩展相同对象,则将导致名称冲突。暴露私有变量会加剧问题,并可能被其他人意外利用。</p>    <p>公开 API 小巧玲珑,其余部分隐身不见,正是避免问题的优秀方案。</p>    <h3><strong>处理私有变量</strong></h3>    <p>JavaScript 中需要通过函数作用域来实现私有变量。需要使用跨作用域私有变量时,人们习惯使用 _ 前缀来标志,并将其公开暴露。这可以避免意外使用,但无法解决命名冲突。更好的办法是使用 ES6 的 Symbol 基本数据类型。</p>    <p>Symbol 是一种唯一的、不可变的数据类型,可用作对象属性标识符。</p>    <p>下面的代码展示了 symbol 的实际使用:</p>    <pre>  <code class="language-javascript">const color = Symbol()    // a middleware  function colorize (elem, state, next) {    elem[color] = 'red'    next()  }</code></pre>    <p>这样一来,通过 color symbol (以及元素 elem)就能获取 red 。 red 的私有程度,可由 color symbol 暴露的不同程度控制。合理数量的私有变量,通过中心存储读取,是一种优雅的解决方案。</p>    <pre>  <code class="language-javascript">// symbols module  exports.private = {    color: Symbol('color from colorize')  }  exports.public = {}</code></pre>    <p>index.js 如下:</p>    <pre>  <code class="language-javascript">// main module  const symbols = require('./symbols')  exports.symbols = symbols.public</code></pre>    <p>在项目内部,所有模块都可访问 symbol 存储对象,但私有部分不会对外暴露。公有部分则可用于对外部开发者暴露一些低层次特性。这就避免了意外使用,因为开发者需要明确引入需要使用的 symbol。此外,symbol 引用也不会像字符串一样产出冲突,是故不会产生命名冲突。</p>    <p>以下几点概括了不同场景下的用法:</p>    <p>1. 公有变量</p>    <p>正常使用.</p>    <pre>  <code class="language-javascript">function (elem, state, next) {    elem.publicText = 'Hello World!'    next()  }</code></pre>    <p>2. 私有变量</p>    <p>项目私有的跨作用域变量,应当在私有 symbol 对象中加入一个 symbol key。</p>    <pre>  <code class="language-javascript">// symbols module  exports.private = {    text: Symbol('private text')  }  exports.public = {}</code></pre>    <p>并在需要的地方引入。</p>    <pre>  <code class="language-javascript">const private = require('symbols').private    function (elem, state, next) {    elem[private.text] = 'Hello World!'    next()  }</code></pre>    <p>3. 半私有变量</p>    <p>低层次 API 的变量,应当在公有 symbol 对象中加入一个 symbol key。</p>    <pre>  <code class="language-javascript">// symbols module  exports.private = {    text: Symbol('private text')  }  exports.public = {    text: Symbol('exposed text')  }</code></pre>    <p>并在需要的地方引入。</p>    <pre>  <code class="language-javascript">const exposed = require('symbols').public    function (elem, state, next) {    elem[exposed.text] = 'Hello World!'    next()  }</code></pre>    <h3> </h3>    <p> </p>    <p>来自:http://www.zcfy.cc/article/writing-a-javascript-framework-project-structuring-risingstack-1675.html</p>    <p> </p>