SegmentFault 前端学习之路最新的文章
2023-03-27T16:39:14+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
从阅读源码到开发一个基于业务的脚手架,全是我的知识盲区?!
https://segmentfault.com/a/1190000043589333
2023-03-27T16:39:14+08:00
2023-03-27T16:39:14+08:00
gyt95
https://segmentfault.com/u/gyt95
2
<blockquote><p>阅读本文你可能获得:</p><p>1.掌握 Node 常用模块的常用 API</p><p>2.掌握开发基于业务的脚手架的流程</p><p>3.了解 npm 包开发中你可能忽略的细节</p><p>4.渗透式 create-vant-cli-app 源码解读</p><p>5.更多...</p></blockquote><h2>起因</h2><p><del>昨天</del>两周前一位同事要改某个旧的功能模块,我看内容很少又是用<code>Vue3</code>写的,迁移到我搭建好了的<code>monorepo</code>项目里刚好合适。于是就让他来试水。</p><p>然后就是一顿操作。。各种小问题,幸好都得以解决。</p><p>虽然解决了,但集成进来的项目会越来越多,就算我不当“客服”,而是直接把初始化步骤写好了,同事也可能怨声载道,毕竟要手动改很多东西。如下图所示。</p><p><img src="/img/remote/1460000043589335" alt="" title=""></p><p>只是截取了一部分,<strong>我最后的结论是这个配置的事情必须搞成自动化。</strong></p><p>因此,还是得把这几个月拖拖拉拉依然没搞出来的“快速初始化项目”的任务给完成。</p><p>但是怎么写呢?你让我手写一个 CLI 真写不出来。除非我平时就是不停地造这类轮子。</p><p>我一直比较喜欢<code>Vant</code>,这次会一边学习源码,一边开发一个基于我们业务的脚手架。</p><h2>准备</h2><p>我们来根据 <a href="https://link.segmentfault.com/?enc=WvwqbuMtXiGPDyC7udpnGA%3D%3D.YONI4jX8pbUvKUVkWRVLXLN68KlU%2FW9KXNBW%2FvssiZAf78cL11UfJTu2gHaRbowIC5rwe3oVbi2tWA5KR6qfb%2BKGg%2FyQEr8%2B41xJ8f5txQo%3D" rel="nofollow">Vant文档说明</a> 看看怎么运行脚手架的:通过<code>yarn create vant-cli-app</code>快速创建项目,同时又支持手动安装,例如<code>pnpm add @vant/cli -D</code>。</p><p>方式不唯一,而且很陌生,不行,开始晕了。</p><p>“简单点,搭建的方式简单点!”</p><p><strong>定下第一个小目标:能通过</strong><code>yarn create ektfe-cli-app</code><strong>命令安装依赖并运行。</strong></p><h2><strong>第一个小目标:本地运行 CLI</strong></h2><h3>yarn create 是什么?原理是什么?</h3><p>先从命令入手,<code>yarn create</code>是什么?原理是什么?</p><p>根据<a href="https://link.segmentfault.com/?enc=l5biFRlQP9nk26DfF3l5Aw%3D%3D.6kPanbC9TFJ2%2BG6igHG1W2d5dmc2%2FR6IMxQ%2BHVR7muJ%2FX2QtM%2BG8vWbtISGBcI%2Fw" rel="nofollow">Yarn 中文文档</a>可知命令格式:</p><p><code>yarn create <starter-kit-package> [<args>]</code></p><p><strong>这个命令其实是简写!</strong> 主要帮助你<strong>同时</strong>做两件事:</p><ul><li>全局安装 <code>create-<starter-kit-package></code>(如果存在就将其更新到最新版)</li><li>运行<code>package.json</code>的<code>bin</code>字段下的可执行文件。并且还会将任何<code><args></code>转发给它</li></ul><p>也就是说,<code>yarn create react-app my-app</code>等价于</p><pre><code class="shell">$ yarn global add create-react-app
$ create-react-app my-app</code></pre><p>注意1:<code><starter-kit-package></code>就是以<code>create-</code>开头的 npm 包。</p><p><strong>(所以</strong><code>create-cli-app</code><strong>的</strong><code>create-</code><strong>是固定的,必须要加!)</strong></p><p>注意2:<code>npm init</code>也是一样用法,例如:<code>npm init react-app my-app</code></p><h3>package.json 参数解读</h3><p><a href="https://link.segmentfault.com/?enc=e0026yn03p47sKPDwNEX1g%3D%3D.brFUq1nAjXG3V96RnCSMWSNDU5dAslQxbFfDSnba0WA5gCWxqzYID2AE%2FaDHyTfMqIMARdoNXHEHhRLekFHCAA%3D%3D" rel="nofollow">npm 英文官方文档</a>(但不全,有部分在 TS 文档、 Node 文档里都有提及)</p><p>看开源项目可先看<code>package.json</code>(明明是<code>README.md</code>!)。</p><p>所以我们来看<code>vant-cli</code>的<code>package.json</code>。</p><p>后记:是的,当时先看<code>vant-cli</code>。我以为我要开发的是这个。。</p><p><img src="/img/remote/1460000043589336" alt="" title=""></p><p>有些字段很关键,有些字段不关键。但还是整体了解一下吧。</p><p><strong>黑体字是经常忘记但很重要的字段。</strong></p><p>name:包名称,发布 npm 包的时候也是这个名称</p><p>version:版本号</p><p><strong>type:</strong> <code>package</code>下的<code>.js</code>被<code>Node</code>以<code>cjs</code>或<code>esm</code>加载。</p><p><strong>main:加载这个 npm 包时的入口文件。</strong></p><p>官方文档称:<code>main</code>字段是一个模块ID,是程序的主要入口点。即如果你的 npm 包名为<code>foo</code>,并且用户安装了它,然后在项目里导入例如<code>require("foo")</code>,那么<code>main</code>模块里<code>export</code>的对象将被返回。</p><p>路径是相对于 npm 包根目录的模块。例如这里<code>lib/index.ts</code>就是执行打包后生成的<code>lib</code>目录中的<code>index.ts</code>。当加载这个 npm 包的时候,会执行<code>index.ts</code>。如果没有指定,默认是根目录的<code>index.js</code>。</p><p><strong>typings:</strong> 起初根本查不到这个字段,只看到 types 字段。于是灵机一动把光标移动到 typings 字段上面👇意思是“typings”字段与“types”同一个意思,使用哪个都行。</p><p><img src="/img/remote/1460000043589337" alt="" title=""></p><p>它们都是用于指定<code>TypeScript</code>项目中导入该 npm 包时使用的类型声明文件(.d.ts 文件)的位置。这个字段可以帮助<code>TypeScript</code>编译器在导入 npm 包时正确地处理类型检查。</p><blockquote>在早期版本的<code>TypeScript</code>中,类型声明文件的扩展名是<code>.d.ts</code>,而<code>typings</code>字段用于指定该文件的位置。后来,<code>TypeScript 2.0</code>引入了<code>types</code>字段,作为<code>typings</code>字段的替代品。因此,如果你在使用较新版本的<code>TypeScript(2.0 及以上)</code>,应该使用<code>types</code>字段。</blockquote><p><strong>bin:</strong> 命令名到本地文件名的一种映射。它允许在安装 npm 包后将脚本添加到系统的 PATH 路径中。这些脚本可以是命令行工具或其他可执行文件。</p><p>当例如<code>vant-cli</code>被全局安装,该文件会被链接到全局<code>bin</code>目录,或者创建一个<code>cmd</code>去执行<code>bin</code>字段里的指定文件,因此它可以按名称运行。</p><p>另外,你必须确保<code>bin</code>字段引用的文件,以<code>#!/usr/bin/env node</code>开头,否则脚本不会被视为可执行文件。</p><p><strong>简单来说,会自动直接执行这个文件,例如 vant-cli 本地安装可以用</strong><code>pnpm add @vant/cli -D</code> <strong>,指的是在当前工程目录的命令行可以执行</strong><code>bin.js</code><strong>这个文件。</strong></p><p>(在安装时,如果是全局安装,npm 将会使用符号链接把这些文件链接到 prefix/bin ,如果是本地安装,会链接到./node_modules/.bin/)</p><p><strong>注意:</strong><code>yarn create vant-cli-app</code><strong>对应的不是</strong><code>vant-cli</code><strong>这个包的</strong><code>name</code><strong>字段(@vant/cli)或者</strong><code>bin</code><strong>字段(vant-cli)。而是</strong><code>create-vant-cli-app</code><strong>这个 npm 包里的</strong><code>bin</code><strong>字段,也就是说</strong><code>yarn create</code><strong>对应的是</strong><code>create-vant-cli-app</code><strong>项目。</strong></p><p><img src="/img/remote/1460000043589338" alt="" title=""></p><h3>区分 yarn create 和 yarn add</h3><p><strong>首先,</strong> <code>yarn create</code><strong>和</strong><code>yarn add</code><strong>是两回事!</strong></p><p><strong>前者是全局安装,会包含两个步骤。后者是本地安装,即安装到当前工程目录里。</strong></p><p>由于<code>yarn create</code>后面参数<code><starter-kit-package></code>是以“create-”开头的 npm 包。所以<code>yarn create vant-cli-app</code>命令之所以能创建项目,实际上是全局安装一个“create-”开头的 npm 包。即这里的<code>create-vant-cli-app</code>!所以你就能在<code>windows</code>的<code>C盘</code>的<code>Yarn</code>安装路径里的<code>bin</code>目录找到了<code>create-vant-cli-app</code>和<code>create-vant-cli-app.cmd</code>两个文件了!</p><p><img src="/img/remote/1460000043589339" alt="" title=""></p><p>然后就会自动执行这个<code>create-vant-cli-app</code>的入口文件,最后在你执行<code>yarn create vant-cli-app</code>命令的目录下帮你初始化一个项目。</p><p><strong>为什么会自动执行?上文提到过,</strong> <code>yarn create</code><strong>包含两个步骤嘛,第一步全局安装,第二步就是执行 bin !</strong></p><p><strong>那</strong><code>yarn add @vant/cli -D</code><strong>又是怎么回事呢?</strong></p><p>其实上面一大段都没提及<code>vant</code>的另一个 npm 包即<code>vant-cli</code>。<code>@vant/cli</code>就是<code>vant-cli</code>的<code>name</code>。我们之前也提到这个就是 npm 包的名称。而<code>bin</code>是当你执行这个 npm 包时会执行对应的文件。</p><p>咦,刚刚好像说<code>main</code>是入口文件呀,那执行的不就是从<code>main</code>开始的吗?那现在又说<code>bin</code>也是执行的字段。</p><p><strong>那到底</strong><code>bin</code><strong>和</strong><code>main</code><strong>本质区别是什么?</strong></p><h3>区分 main 和 bin 字段</h3><p><strong>再次回顾</strong><code>main</code><strong>和</strong><code>bin</code><strong>字段:(当你抱怨“这两个字段刚刚不是说了吗”时,恭喜你,你掌握得很牢固)</strong></p><p><code>main</code>:当用户<code>install</code>某个 npm 包后并在代码里引入时,此时会进入<code>main</code>字段所指定的文件。</p><p><code>bin</code>:当全局安装某个 npm 包时,会链接到全局<code>bin</code>目录,像<code>xxx.cmd</code>就会创建一个<code>cmd</code>然后里面就执行这个<code>bin</code>字段对应的文件。</p><p><strong>因此!!</strong> <code>bin</code><strong>所对应的文件,执行时机是在全局安装或本地安装的时候。而</strong><code>main</code><strong>字段所对应的文件,执行时机是在代码中导入的时候。这就是两者最大区别,两者看起来都在执行着什么,但执行的位置、时机都不同。</strong></p><p><strong>总算是理清了,个人觉得以上几个字段是最重要的。一定要区分清楚。希望不只有我是今天才分清的。</strong></p><h3>其它参数解读</h3><p>engines:指定运行你这个包的 Node 版本(有可能有的功能是 Node 的某个版本之后才支持)(如果不指定,就是任何 Node 版本都可以)(也支持指定 npm 版本)</p><p>scripts:脚本命令</p><p>files:定义发布到npm仓库中的包时,应该包含哪些文件和目录。这个字段的作用是告诉npm在发布包的时候只包含指定的文件和目录,避免将不必要的文件或目录发布到npm上。同时,也会作为依赖项安装到项目工程里</p><p>keywords:关键字集合。一个字符串数组。好处:有助于别人通过 npm 搜索到你的包。(description同理)</p><p>publishConfig:发布npm包的访问权限(public)、发布地址</p><p>repository:项目地址</p><p>bugs:bug反馈地址</p><p>author:作者。只有一个人。另,contributors 是数组</p><p>devDependencies:开发环境会用到的依赖</p><p>dependencies:生产环境会用到的依赖</p><p>有的组件库还会有 module 字段,是打包生成一个 esm 语法的目录,module 字段就指向对应的入口文件。</p><p><strong>至此,我(们)终于明白:发布 npm 包时如何指定文件/目录,安装 npm 包时执行 npm 包里的哪个文件,导入到项目工程后又会执行哪个文件。</strong></p><h3>如何本地开发 CLI ?</h3><p>执行一下<code>vant-cli</code>的<code>dev</code>脚本命令,它实际上是执行<code>tsc -w</code>。</p><p>已知<code>tsc</code>是用来编译<code>.ts</code>文件的。默认从当前目录开始编译,而这个也取决于<code>tsconfig.json</code>。</p><h4>为什么要用 tsconfig.json ?</h4><p>因为实际开发的项目,很少是只有单个文件,当我们需要编译整个项目时,就可以使用<code>tsconfig.json</code>文件,将需要使用到的配置都写进<code>tsconfig.json</code>文件,这样就不用每次编译都手动输入配置,另外也方便团队协作开发。</p><p>然后可以看到<code>create-vant-cli-app</code>的<code>tsconfig.json</code>是这样写的👇</p><pre><code class="json">{
"extends": "../../tsconfig",
"compilerOptions": {
"target": "ES2019",
"outDir": "./lib",
"module": "commonjs",
"declaration": true
},
"include": ["src/**/*"]
}</code></pre><p>可以看出<code>outDir</code>目录是<code>./lib</code>。所以当执行<code>dev</code>命令时,<code>tsc -w</code>就会监听<code>include</code>字段里所有<code>.ts</code>文件,一旦有文件变化,就编译输出到<code>./lib</code>目录。</p><p>include 属性作用:指定编译需要编译的文件或目录。</p><p>ok,开始初始化项目。</p><h3>初始化自己的 CLI</h3><p>补充一下,最终生成的模板长这样:</p><p><img src="/img/remote/1460000043589340" alt="" title=""></p><h4>配置 package.json</h4><p>通过<code>pnpm init</code>等命令就能初始化了,再进行一些修改。</p><p>结合前文知识,我们知道:</p><p>main:项目里导入时,会进入的入口文件。</p><p>bin:执行<code>yarn create ektfe-cli-app</code>时,全局安装后,会执行的文件对应路径。</p><pre><code class="json">{
"name": "create-ektfe-cli-app",
"version": "0.0.1",
"description": "Create CLI App",
"main": "lib/index.js",
"bin": {
"create-ektfe-cli-app": "./lib/index.js"
},
"scripts": {
"dev": "tsc --watch"
},
"author": "gyt95",
"license": "MIT"
}</code></pre><p>而<code>scripts</code>的<code>dev</code>:运行项目,并<code>watch</code>变化,一旦变化就重新编译、输出。输出到哪里呢?这里就要配合<code>tsconfig.json</code>了。</p><pre><code>{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2019",
"outDir": "./lib",
"module": "commonjs",
"declaration": true,
},
"include": ["src/**/*"]
}</code></pre><p>然后创建<code>src</code>目录,创建<code>index.ts</code>,输入👇</p><pre><code>#!/usr/bin/env node
function init(){
console.log(666)
}
init()</code></pre><p>报错:无法在 --isolatedModules 下编译 "index.ts",因为它被视为全局脚本文件。请添加导入、导出或空的 "export {}" 语句来使它成为模块。ts(1208)</p><p><strong>为什么会报错?</strong></p><p>当我们的<code>tsconfig.json</code>中的<code>isolatedModules</code>设置为<code>true</code>时,如果某个<code>.ts</code>文件中没有<code>import</code>或者<code>export</code>时,<code>ts</code>则认为这个模块不是一个<code>ES Module</code>模块,它被认为是一个全局的脚本,这个时候在文件中添加任意一个<code>import</code>或者<code>export</code>都可以解决这个问题。</p><p>所以这里加上<code>export {}</code>就不会报错了。</p><pre><code class="ts">#!/usr/bin/env node
function init(){
console.log(666)
}
init()
export {}</code></pre><h4>为什么要设置 isolatedModules 为 true?</h4><p>假设有如下两个<code>ts</code>文件,我们在<code>a.ts</code>中导出了<code>Test</code>接口,在<code>b.ts</code>中引入了<code>a.ts</code>中的<code>Test</code>接口。</p><p>然后又在<code>b.ts</code>将<code>Test</code>给导出。</p><pre><code class="ts">export interface Test {}</code></pre><pre><code class="ts">import { Test } from './a';
export { Test };</code></pre><p><strong>这会造成一个什么问题呢?</strong></p><p>如<code>Babel</code>对<code>ts</code>转义时,它会先将<code>ts</code>的类型给擦除,也就是<code>a.ts</code>空了。但是当碰到<code>b.ts</code>文件时,<code>Babel</code>并不能分析出<code>export { Test }</code>它到底导出的是一个类型还是一个实实在在的<code>js</code>方法或者变量,这时候<code>Babel</code>选择保留<code>export</code>。</p><p>但是<code>a.ts</code>文件在转换时可以很容易的判定它就导出了一个类型,在转换为<code>js</code>时,<code>a.ts</code>中的内容将被清空,而<code>b.ts</code>中导出的<code>Test</code>实际上是从<code>a.ts</code>中引入的,这时候就会产生报错。</p><p><strong>如何解决上述问题?</strong></p><p><code>ts</code>提供了<code>import type</code>或<code>export type</code>,用来明确表示我引入/导出的是一个类型,而不是一个变量或者方法,使用<code>import type</code>引入的类型,将在转换为<code>js</code>时被擦除掉。</p><pre><code class="ts">import { Test } from './a';
export type { Test }; </code></pre><p>其实就是基于“TS的导入省略能力会检测如果导入类型就会在编译后被擦除”这一特性上,擦除后导致<code>JS</code>找不到这个类型了,于是报错。所以,假如设置为<code>true</code>就不会在非<code>tsc</code>编译器有这个情况,假如是<code>false</code>,就要你自己加<code>import type</code>才行(3.8 新出的特性)</p><p>之前其实有了解过这个知识点。TypeScript 5.0 新特性<code>--verbatimModuleSyntax</code>要求你明确写类型导入,才会保留。否则,就会擦除。任何没有<code>type</code>修饰符的导入或导出,都会被保留。而任何使用<code>type</code>修饰符的内容,都会被删除。</p><p>即:“所有非仅类型导入/导出都会被保留,而仅类型导入/导出都会被移除。”</p><p>以前还有其它属性:</p><p><code>--importsNotUsedAsValues</code>:用于确认类型导入的使用(仅类型导入需要被显式标记,而未被使用的值导入仍然将会保留)</p><p><code>--preserveValueImports</code>:用于显式避免部分导入语句的移除(所有值导入都将被完整保留,避免 TypeScript 无法检测其使用方式的情况)</p><p>由于以前当同时开启<code>preserveValueImports</code>和<code>isolatedModules</code>配置时,<code>isolatedModules</code>会让引入的类型必须是<code>type-only</code>。所以来自同一个文件的数据必须得分两条<code>import</code>引入。</p><pre><code class="ts">import { someFunc, BaseType } from "./some-module.js";
// ^^^^^^^^
// Error: 'BaseType' is a type and must be imported using a type-only import
// 除非
import type { BaseType } from "./some-module.js";
import { someFunc } from "./some-module.js"</code></pre><p>而<code>TypeScript 4.5</code>允许一个<code>type</code>修饰词在<code>import</code>语句中👇</p><pre><code class="ts">import { someFunc, type BaseType } from "./some-module.js";</code></pre><p><code>isolatedModules</code>是为了避免非<code>tsc</code>的编译器编译时出现类型被擦除但<code>import</code>的类型仍存在导致报错的问题。所以开启这个就为了安全编译,必须是模块隔离的。模块隔离,指的是导入和导出都是确定的,不是模棱两可的。是类型就必须声明是类型。</p><p>但很多开源库都不开启这个,不太理解。。就正如我暂时不理解为什么有<code>tsc</code>还需要用<code>Babel</code>编译<code>ts</code>代码?</p><p><strong>小结:如果设置</strong><code>isolatedModules</code><strong>为</strong><code>true</code> <strong>,那么新建的</strong><code> .ts </code><strong>文件一定要包含一个</strong><code>import</code><strong>或者</strong><code>export</code> <strong>。</strong></p><h4>执行 dev 命令运行 CLI</h4><p>执行<code>dev</code>脚本后,<code>tsc --watch</code>后生成<code>lib</code>目录</p><p><img src="/img/remote/1460000043589341" alt="" title=""></p><h4>如何本地调试?</h4><p>以前一般我们都用<code>npm link</code>来调试组件库。现在用<code>yarn</code>应该也一样,看了下 <a href="https://link.segmentfault.com/?enc=XWpLyHoHuVh2M4%2FGfV1Yyg%3D%3D.j5lXnnEm4WMpjCQNp29G688t0wUrd6J6Q8tOcAyVJAYM3CRZaWrrC%2FmFL%2FXDqnCCkKFuplKGnU%2BetEMTyyhR3A%3D%3D" rel="nofollow">Yarn官方文档</a> 。是用的<code>yarn link</code>。注意如果<code>yarn unlink</code>就失去了全局链接,此时再<code>yarn create</code>就会去线上<code>yarnpkg</code>查找。</p><p>pnpm 也有<code>pnpm link</code>,具体可见 <a href="https://link.segmentfault.com/?enc=oF1BM3T0att7SwgtVeOK1Q%3D%3D.%2Fsx0boVpnHmziLEDriAsB%2FFsqVyFtc5Am8yFeIiWF94%3D" rel="nofollow">Pnpm官方文档</a> 。</p><pre><code>pnpm link <dir>
从执行此命令的路径或通过 <dir> 指定的文件夹,链接package到node_modules中。
pnpm link --global
从执行此命令的路径或通过 <dir> 选项指定的文件夹,链接package到全局的node_modules中,所以使其可以被另一个使用pnpm link --global <pkg> 的package引用。
pnpm link --global <pkg>
将指定的包(<pkg>)从全局 node_modules 链接到 package 的 node_modules,从该 package 中执行或通过 --dir 选项指定。</code></pre><p>由于我没有把 pnpm 加入到 PATH ,所以使用 全局link 就报错了👇</p><p><code>The configured global bin directory "C:\Users\xxxx\AppData\Local\pnpm" is not in PATH</code></p><p>有<code>yarn create</code>就行,目前为止,我们终于实现了第一个小目标!</p><h2>第二个小目标:增加询问</h2><p>内置一个模板 & 围绕业务提供几个问题。</p><h3>主要用到的库</h3><p>已知从<code>index.ts</code>执行。看看<code>create-vant-cli-app</code>的代码</p><pre><code class="ts">#!/usr/bin/env node
import consola from 'consola';
import { prompt } from 'inquirer';
import { ensureDir } from 'fs-extra';
import { VanGenerator } from './generator';
const PROMPTS = [
{
type: 'input',
name: 'name',
message: 'Your package name',
},
];
async function run() {
const { name } = await prompt(PROMPTS);
try {
await ensureDir(name);
const generator = new VanGenerator(name);
await generator.run();
} catch (e) {
consola.error(e);
}
}
run();</code></pre><p>看到<code>consola</code>,这看起来就是用于控制台打印输出的。</p><h4>consola</h4><p><a href="https://link.segmentfault.com/?enc=fTI41kepTHgNCEvzwvAMOQ%3D%3D.BW%2BEFsPWytT9WjNV0OiQMxhmUc3F6NWJPq7sayRR5UY%3D" rel="nofollow">https://github.com/unjs/consola</a></p><p>官方描述:用于<code>Node.js</code>和浏览器的优雅控制台记录器。</p><p><code>pnpm add consola</code>,用起来很简单。</p><pre><code class="ts">const consola = require('consola')
// See types section for all available types
consola.success('Built!')
consola.info('Reporter: Some info')
consola.error(new Error('Foo'))</code></pre><p>控制台展示:</p><p><a href="https://link.segmentfault.com/?enc=QjwRroCLrgzuo7xSATB0PQ%3D%3D.CQGUjV3mRnd5rTNBRYboVAkwwy0L%2FbbkXz3SGPUxftORqx%2Fp3cGGWhgsbXGgRmoNfHWLaKuATiynctXJTscePXLzYziPrrxiqBh%2FO5VNRorvr9SEWhy53wEBWmRf7swhZ6k8graX4Jiuy7sPhbZMvA%3D%3D" rel="nofollow"><img src="/img/remote/1460000043589342" alt="" title=""></a></p><p>下一个是<code>inquirer</code>。</p><h4>inquirer</h4><p><a href="https://link.segmentfault.com/?enc=kK0hCgWLQdnWPD9%2B%2BYyl6Q%3D%3D.ZrAdo2s9FS1wasAnpgkgrBfJiJPFdijd%2B4K1ow3aSB%2BgoJnGVV64KVu2x1bLiWRp" rel="nofollow">https://github.com/SBoudrias/Inquirer.js</a></p><p><code>pnpm add inquirer</code></p><p>基本用法:传入一个“问题”数组给<code>inquirer</code>的<code>prompt</code>函数。并异步获取结果。获取到的结果就是用户选择的选项。</p><p>代码如下:</p><pre><code class="ts">import { prompt } from 'inquirer';
const PROMPTS = [
{
type: 'input',
name: 'name',
message: 'Your package name',
},
];
async function run() {
const { name } = await prompt(PROMPTS);
}
run();</code></pre><p>当调用<code>prompt</code>函数后,如果正常调用,则会再调用<code>ensureDir</code>函数。这个函数来自另一个库<code>fs-extra</code>。</p><h4>fs-extra</h4><p><a href="https://link.segmentfault.com/?enc=tIzWbhaHfhQksLa9t7W%2FQQ%3D%3D.O1sJidoCNIeXTGpKCU5DCiq0miw4pA0%2Bw8XtPD7yAZ3dzXhg0vMNcb6OHosnmo5q" rel="nofollow">https://github.com/jprichardson/node-fs-extra</a></p><p>官方描述:<code>fs-extra</code>添加了原生<code>fs</code>模块中未包含的文件系统的方法,并为<code>fs</code>方法添加了<code>promise</code>支持。它还使用<code>graceful-fs</code>来防止<code>EMFILE</code>错误。应该是<code>fs</code>的替代品。</p><p>那么<code>create-vant-cli-app</code>源码中的<code>await ensureDir(name)</code> 意思是确保 name 这个目录存在。如果目录结构不存在,则创建它。然后通过<code>new Template(name)</code>创建一个模板实例。并通过<code>run()</code>执行。</p><p>写了个简单逻辑:</p><pre><code class="ts">#!/usr/bin/env node
import consola from 'consola'
import inquirer from 'inquirer'
const PROMPTS = [
{
type: 'input',
name: 'name',
message: '你需要创建的项目名称叫什么?'
},
{
type: 'list',
name: 'type',
choices: ['nsft', 'say', '其它'],
message: '当前项目属于哪个平台的?'
},
]
async function run (){
const result1 = await inquirer.prompt(PROMPTS)
console.log(result1);
try {
} catch (e) {
consola.error(e)
}
}
run()</code></pre><p>然后执行<code>dev</code>命令再<code>yarn link</code>命令。</p><p>然后到目标项目中执行<code>yarn create ektfe-cli-app</code>。结果报错。</p><p><img src="/img/remote/1460000043589343" alt="" title=""></p><p>报错:Instead change the require of inquirer.js in C:\xxxx\xxx\xxx\index.js to a dynamic import() which is available in all CommonJS modules.</p><p>意思是<code>inquirer.js</code>的<code>require</code>要更改为在所有<code>cjs</code>模块中都可用的动态<code>import()</code></p><p>代码明明写的是<code>import</code>。那一定是因为<code>tsconfig.json</code>。于是把<code>"module": "commonjs"</code>注释掉。</p><p>但又提示:</p><p><img src="/img/remote/1460000043589344" alt="" title=""></p><p><strong>不能在模块外使用 import 语句?</strong></p><p>查资料找到相应解决办法:<a href="https://link.segmentfault.com/?enc=yjLYJD8E2nHDJY1OduVEUQ%3D%3D.WcvWQxLygkkDRO6V6ixfMY7F70akLtXWxTxqtXVWXL7o0Mgm7XTsb9Gaj%2F84a89BMfOxSabMI%2FGT9qI1%2FvdB9SHPEMq3DOYuO7WYhcdptRpVJI0N9wSGfhr2iu2SEpDP" rel="nofollow">https://bobbyhadz.com/blog/javascript-syntaxerror-cannot-use-...</a></p><p>即要在<code>package.json</code>中添加<code>type: "module"</code>。回顾上文可知,字段意思是当前<code>package</code>下的<code>.js</code>被<code>Node.js</code>以<code>cjs</code>或<code>esm</code>加载。</p><p><strong>当前流程:</strong></p><p>1.我们写的<code>.ts</code>文件会通过<code>tsc</code>编译输出到<code>lib</code>目录,例如<code>index.ts</code>会编译为<code>./lib/index.js</code>,然后通过<code>yarn link</code>进行全局链接。</p><p>2.在另一个项目里我们通过<code>yarn create ektfe-cli-app</code>全局安装和执行 CLI 。此时所执行的是<code>bin</code>字段对应的文件<code>./lib/index.js</code>。</p><p>3.由于是在<code>Node</code>环境执行的,但此时我们用的语法是<code>esm</code>的<code>import</code>,所以我们必须通过<code>type: "module"</code>告诉<code>Node</code>当前 npm 包里的<code>.js</code>都会作为<code>es</code>模块加载。</p><p>你也可以把鼠标移动到 package.json 的 type 字段上,会有以下提示:</p><p>当设置为“module”时,type 字段允许包指定所有 .js 文件都是 ES 模块。如果“类型”字段被省略或设置为“commonjs”,则所有 .js 文件都被视为 CommonJS。</p><p>而最尴尬的是一开始我就是想直接用默认的<code>cjs</code>模块规范。结果<code>inquirer</code>又要我用<code>esm</code>语法<code>import</code>。</p><p><strong>怎么会这样?!</strong></p><p>打开<code>inquirer</code>的 <a href="https://link.segmentfault.com/?enc=7RNy06kR5NDkM8YuqCvEmw%3D%3D.wguUHJaoGLjC7hp0Zv7OwIRpBo%2B9rIj6%2BCZZscPO70UsVkYfboR4X2Z%2FGl4sSJcJ" rel="nofollow">npm 包网址</a>(<a href="https://link.segmentfault.com/?enc=SYlmCUzE2INCz3Y%2BJ9kHfw%3D%3D.h%2Fek%2BY3ZYBKV53RU%2BQTQ4O0wKYWiDQ0jSgTNsnYXGS9M7Emn0f0TkFm9wNF7VhLMTnALlbGIn%2Fm4KG%2F5W1u07g%3D%3D" rel="nofollow">Github</a>也行),全局搜索“commonjs”。</p><blockquote>Inquirer v9 and higher are native esm modules, this mean you cannot use the commonjs syntax require('inquirer') anymore. If you want to learn more about using native esm in Node, I'd recommend reading <a href="https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c">the following guide</a>. Alternatively, you can rely on an older version until you're ready to upgrade your environment:<code>npm install --save inquirer@^8.0.0</code></blockquote><p>意思是<code>inquirer</code>版本如果是 9 或以上,那么就是原生<code>esm</code>模块。意味着你不能再用<code>cjs</code>语法<code>require('inquirer')</code>。如果你想学习更多关于在 Node 中使用原生 esm 的知识,我将推荐阅读<a href="https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c">这个指南</a>,或者你可以用旧版本,直到你准备好升级你环境为止。</p><p><strong>那怎么</strong><code>create-vant-cli-app</code><strong>又没问题?</strong></p><p>去看它的<code>package.json</code>,<code>inquirer</code>版本是 8+。</p><p>(因为我之前拉取<code>Vant</code>的源码到本地,也没同步后续代码,所以还是用的<code>inquirer</code>。后来我又到<code>Github</code>再看,发现已经改为<code>enquirer</code>了。基本上就是对<code>inquier</code>的重新实现)</p><p><img src="/img/remote/1460000043589345" alt="" title=""></p><p><strong>也就是说,当前我用的</strong><code>inquirer</code><strong>版本是 9 ,只能是</strong><code>package.json</code><strong>指定</strong><code>type</code><strong>为</strong><code>module</code> <strong>。</strong></p><p><img src="/img/remote/1460000043589346" alt="" title=""></p><h3>构建询问流程,生成模板项目</h3><blockquote>制定一个基本流程👇(属于草稿版,开发过程中不断完善,所以有出入)</blockquote><p>输入项目名称? > 数据图表</p><p>输入<code>html</code>名称? > data-charts</p><p>选择:所属平台? > nsft</p><p>其它:输入具体名称?</p><p>(如果输入非a-zA-Z则提示要输入英文单词)</p><p>(默认全转小写,而且不能和上面的同名,否则警告)</p><p>内部自动分配端口号,要改<code>data.json</code></p><p>内部自动创建<code>module</code>目录,创建<code>data-charts.html</code>,内部<code>title</code>添加“数据图表”</p><p>内部自动创建<code>src</code>目录,对于<code>App.vue</code>要进行修改</p><p>内部创建<code>package.json</code>,修改对应的<code>name</code></p><p>...</p><h4>阶段1:询问后创建目录</h4><p>我希望用户选择了问题3的“其它”时,会提供一个输入框让用户输入。</p><p>实现方式比较简单粗暴。代码如下:</p><pre><code class="ts">#!/usr/bin/env node
import consola from 'consola'
import inquirer from 'inquirer'
import fs from 'fs-extra'
async function run() {
const { name, htmlName } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: '你需要创建的项目名称叫什么?'
},
{
type: 'input',
name: 'htmlName',
message: '新项目对应的 html 名称叫什么?'
}])
if (!name || !htmlName) return;
const { type } = await inquirer.prompt([
{
type: 'list',
name: 'type',
choices: [
{ name: 'nsft', value: 'nsft' },
{ name: 'SAY', value: 'say' },
{ name: '其它', value: 'others' },
],
message: '当前项目属于哪个平台的?'
}])
if (!type) return;
if (type === 'others') {
const { newPlatformName } = await inquirer.prompt([
{
type: 'input',
name: 'newPlatformName',
message: '新的平台叫什么?'
}])
if (!newPlatformName) return;
console.log(`\n正在为你创建新的平台 ${newPlatformName} 目录...`)
fs.ensureDir(newPlatformName)
}
}
run().catch(e => {
consola.error(e)
})</code></pre><p>来看看效果:</p><p><img src="/img/remote/1460000043589347" alt="" title=""></p><p>对应目录下成功创建了平台。</p><p><img src="/img/remote/1460000043589348" alt="" title=""></p><p>这只是第一步。</p><h5>优化1:当前用户如果选择的是已有平台,那么即将创建的目录会到对应平台。否则,会让用户自己输入一个,然后自动创建。</h5><h5>优化2:判断当前根目录的 <code>data.json</code> 中的 <code>PORT</code> 列表,是否包含这个<code>htmlName</code> ,如果是,则提示有重名,需要用户重新输入。否,则继续。</h5><p>因此,针对优化1,流程要做修改。应该是先选择平台,检测后,再输入<code>htmlName</code>值。</p><p>然后就出现一些细节问题。</p><p><strong>如何检测重名?</strong></p><p>通过<code>require('./data.json')</code>拿到<code>json</code>数据,再通过<code>PORT[type][htmlName]</code>检测对应系统中是否包含同名的版块,是,则创建失败。否,则开始生成项目。</p><pre><code class="ts">if(type !== 'others'){
try {
let json = fs.readFileSync('data.json', 'utf-8') // 直接用 fs.readFileSync
const { PORT } = JSON.parse(json);
if(PORT[type][htmlName]){
consola.error(`创建失败!当前所选择的 ${type} 平台存在同名的 html 名称`)
}
} catch (error) {
consola.error('当前目录下找不到data.json')
}
}
// 开始生成项目
// ... </code></pre><p>注意:<code>fs.readFileSync</code>方法获取的值默认是<code>buffer</code>类型,所以传第二个参数<code>utf-8</code>才能返回我们要的数据。</p><p>针对优化2,又有细节问题。</p><p>因为不同平台属于不同工作区。那当前<code>PORT</code>是没有划分工作区,即所有平台的子项目的端口号都混淆在一个对象里。后期维护管理都不方便。所以这里我把<code>PORT</code>再次做了细分。</p><p>除了细分,还添加了一段逻辑:遍历<code>PORT</code>所有<code>key</code>,找到符合当前用户选择的平台,如果不存在,直接创建,并且端口号按照规则自增1000。如果已存在,则找到对应的<code>key</code>的最后一个子的<code>key</code>,拿到对应的端口号,并再按规则自增。这里涉及到<code>fs</code>模块的<code>readFile</code>和<code>writeFile</code>。</p><p>注意:最后<code>writeFile</code>时,要用<code>JSON.stringify</code>转换成字符串形式。</p><h4>阶段2:内部自动生成html</h4><p>难点:html 是自动生成、名字取决于用户输入的<code>htmlName</code>值,内部模板固定,但要嵌入用户输入的项目名称。</p><h5>__dirname</h5><p>直接使用发现报错:__dirname is not defined in ES module scope 。</p><p><strong>以前</strong> <code>__dirname</code> 在<code>Node</code>脚本中十分常用,因为可以获取当前<code>JavaScript</code>文件所在文件夹的路径。</p><p><strong>但是如今!</strong> 如果在<code>es</code>模块下就无法直接使用<code>__dirname</code>了。(上文提到<code>package.json</code>里因为<code>inquirer.js</code>9.x 的原因而被迫写<code>"type": "module"</code>,即采用<code>esm</code>语法。</p><p><strong>解决办法</strong>:需要从原生<code>Node</code>模块<code>url</code>模块导入<code>Node</code>的<code>url</code>和<code>fileURLToPath</code>函数,然后可以通过以下方式自己搞一个和<code>__dirname</code>作用一样的值。</p><pre><code class="ts">const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);</code></pre><p>参考:<a href="https://link.segmentfault.com/?enc=XBu31ZOcdA0kjpYfLV2GIA%3D%3D.EuOz8%2FafYnyqXJobpAPWCuHuPpFYTLSEAWebmZSWJ11ntcYbwn0OaLBctqIiSA9mc%2FhXQVU7L2nnu0T2mrNZpJx4T6sd5C%2FgavzFCJ4WPxs%3D" rel="nofollow">fix-dirname-not-defined-es-module-scope - flaviocopes.com</a></p><p>虽然知道可以这么做,但我不知道这两个函数有什么用啊!赶紧去官方文档看一下。</p><h5>fileURLToPath</h5><p>作用:把 文件url 转换为 本地文件路径 。</p><p>像<code>import.meta.url</code>是当前<code>vant-cli-app</code>的<code>bin/index.js</code>这个文件的绝对路径。</p><pre><code class="ts">console.log(import.meta.url);
const __filename = fileURLToPath(import.meta.url)
console.log(__filename);
// file:///C:/disk_D/packages/create-ektfe-cli-app/lib/index.js
// C:\disk_D\packages\create-ektfe-cli-app\lib\index.js</code></pre><p><strong>奇怪,怎么就会有</strong><code> file:///</code><strong>呢?</strong></p><h5>file:/// 是什么?</h5><p><code>file:///</code>是一个 URL 协议,用于表示本地文件系统上的文件路径。在<code>Node.js</code>中,当我们读取本地文件时,文件路径通常是以<code>file:///</code>开头的 URL。</p><h5>file:// 和 file:/// 有什么区别?</h5><p><code>file://</code>和<code>file:///</code>都是用于表示本地文件路径的 URL 协议,其中<code>file:///</code>是<code>file://</code>协议的扩展,用于更严格地表示文件路径。</p><p><code>file://</code>是一种通用的文件 URL 协议,可以用于表示不同操作系统上的文件路径。例如:</p><ul><li>在<code>Windows</code>中,文件路径可能是<code>file:///C:/path/to/file</code>;</li><li>在<code>macOS</code>中,文件路径可能是 <code>file:///Users/user/Documents/example.txt</code>。</li></ul><p>在这种情况下,我们可以使用<code>file://</code>URL协议来表示文件路径,它在不同操作系统上都可以使用。</p><p>但是,在某些情况下,<code>file://</code>协议可能会有一些问题。例如,当文件路径中包含空格或非 ASCII 字符时,<code>file://</code>协议可能无法正确地解析文件路径。为了解决这个问题,<code>file:///</code>协议引入了更严格的语法来表示文件路径,以确保它们可以被正确地解析。</p><p>具体来说,<code>file:///</code>协议要求文件路径必须满足以下要求:</p><ul><li>文件路径必须是绝对路径。</li><li>文件路径中的空格和非 ASCII 字符必须进行 URL 编码,例如空格应该被编码为<code>%20</code>。</li></ul><p>因此,当我们使用<code>file:///</code>协议来表示文件路径时,可以更加严格地保证文件路径的正确性。</p><p>但我仍然无法理解像<code>file:///Users/user/Documents/example.txt</code>这样的URL,<strong>它的三道杠</strong><code> /// </code><strong>是怎么解析的?</strong></p><h5>解析 file:/// 开头的文件路径</h5><p>在<code>Node.js</code>中,可以使用内置的<code>url</code>模块来解析 URL。针对<code>file:///Users/user/Documents/example.txt</code>这个 URL,可以使用<code>url</code>模块的<code>parse</code>函数来解析该 URL,并获取其中的各个部分。具体代码如下:</p><pre><code class="ts">const url = require('url');
const fileUrl = 'file:///Users/user/Documents/example.txt';
const parsedUrl = url.parse(fileUrl);
console.log(parsedUrl.protocol); // 'file:'
console.log(parsedUrl.pathname); // '/Users/user/Documents/example.txt'</code></pre><p>在上面的代码中,我们首先引入了<code>url</code>模块,并定义了一个<code>fileUrl</code>变量,它包含要解析的 URL。然后,我们使用<code>url.parse</code>函数将<code>fileUrl</code>解析为一个 URL 对象。解析后,我们可以通过访问<code>protocol</code>和<code>pathname</code>属性来获取 URL 的协议和路径信息。</p><p>注意:在解析<code>file:///</code>协议的 URL 时,<code>url.parse</code>函数会将<code>file:///</code>协议解析为<code>file:</code>,并将<code>/</code>视为路径的一部分。因此,在解析后的 URL 对象中,路径信息存储在<code>pathname</code>属性中,且路径前会自动添加一个<code>/</code>符号。</p><p><strong>即</strong><code>file:</code><strong>后的</strong><code> /// </code><strong>会拆分为</strong><code> // </code><strong>和</strong><code>/</code><strong>,</strong> <code>/</code><strong>会变成路径的一部分。所以在</strong><code>pathname</code><strong>里前缀添加</strong><code>/</code><strong>符号。</strong></p><h5>解析 file:// 开头的文件路径</h5><p>与<code>file:///</code>协议不同,<code>file://</code>协议不要求文件路径必须是绝对路径,并且不需要对空格和非 ASCII 字符进行 URL 编码。因此,在解析<code>file://</code>协议的 URL 时,我们需要针对具体的 URL 规范进行解析。</p><p>一种常见的解析方法是,使用正则表达式来匹配 URL 中的各个部分。</p><pre><code class="ts">const fileUrl = 'file://Users/user/Documents/example.txt';
const parsedUrl = fileUrl.match(/^file://([^/]+)(/.*)?$/);
console.log(parsedUrl[1]); // 'Users'
console.log(parsedUrl[2]); // '/user/Documents/example.txt'</code></pre><p>正则表达式包括两个分组,分别用于匹配主机名和文件路径。</p><p><code>^</code>表示字符串开始。</p><p><code>file://</code>表示file://字符串。</p><p><code>([^/]+)</code>,<code>^</code>指的是“非”,<code>+</code>指的是至少1个。所以这里表示多个非<code>/</code>的字符。括号表示一个捕获组。</p><p><code>(/.*)?</code>,<code>/</code>指的是<code>/</code>,结合起来就是<code>/.*</code> ,例如上面的<code>example.txt</code>。一个可选的捕获组,匹配一个以 <strong>/</strong> 开头,后面跟着任意字符的字符串,括号表示一个捕获组,<code>?</code>表示该组为可选,即字符串中可能不存在该部分</p><p>在正则表达式中,<code>.</code>表示匹配任何单个字符,而<code>*</code>表示匹配前一个字符0次或多次。所以,<code>.*</code>组合在一起表示匹配任意数量的任何字符(包括0个字符),直到遇到下一个匹配规则或者字符串的结尾。</p><p>那么问题来了:<code>fileURLToPath(import.meta.url)</code>中的<code>import.meta.url</code>是啥?</p><h5>import.meta.url</h5><p>首先,<code>import.meta</code>是一个在<code>es</code>模块内部可直接使用的对象。它包含的是关于模块运行环境的信息。运行环境可以是浏览器,也可以是<code>Node</code>。</p><p>其次,<code>import.meta</code>对象是可扩展的,宿主(浏览器/<code>Node</code>)可以把任何有用的信息写进去。</p><p>因此,浏览器和<code>Node</code>都给<code>import.meta</code>写入<code>url</code>。</p><p><strong>所以</strong><code>import.meta.url</code><strong>就是你运行那个文件的绝对路径,但是只是个</strong><code>url</code> <strong>。</strong></p><p>一般会通过<code>fileURLToPath</code>这个函数,进行转换。</p><p><code>file:///C:/xxx/xxx/bin/index.js</code>👉<code>C:\xxx\xxx\bin\index.js</code></p><p>然后像<code>vite</code>源码的<code>create-vite</code>中,<code>src/index.ts</code>里有一行<code>path.resolve(filename, '../..', template-${template})</code>,要拿到某个<code>template</code>模板目录进行拼接。(<code>src</code>和<code>template-xx</code>是同级的)</p><p>通过<code>fs.readdirSync(templateDir)</code>读取目录。得到的<code>files</code>,遍历,通过<code>write</code>函数写入。</p><p><code>write</code>函数内部,会通过<code>path.join</code>拼接<code>root</code>和<code>file</code>。<code>root</code>是<code>path.join(cwd, targetDir)</code>,而<code>cwd</code>就是<code>process.cwd()</code>。</p><h5>process.cwd()</h5><p>官方文档描述:返回 Node.js 进程的当前工作目录。</p><p><strong>就是你执行</strong><code>yarn create vant-cli-app</code><strong>的当前目录的绝对路径。</strong></p><p>啊!这个才是我想要的!就是想要获取当前所在的目录。</p><p><strong>奇怪!那上文的 __dirname 是?</strong></p><p>它是当前正在执行的文件的目录的绝对路径。也就是说,当前我执行的<code>yarn create vant-cli-app</code>,对应执行的是<code>bin</code>字段指向的文件,而这个文件的绝对路径,才是<code>__dirname</code>的值!</p><p>这里的<code>__dirname</code>是通过<code>path.dirname</code>接收<code>__filename</code>来得到的。</p><h5>path.dirname</h5><p>官方描述:获取传入的文件的父目录。</p><p>结合下图应该更明白了!</p><p><img src="/img/remote/1460000043589349" alt="" title=""></p><h5>区分 process.cwd 和 __dirname</h5><p>process.cwd:当前你<strong>执行命令所在的目录</strong>的绝对路径</p><p>__dirname:当前<strong>正在执行的文件所在目录</strong>的绝对路径</p><p>ok!回到上面<code>create-vite</code>的<code>path.join(cwd, targetDir)</code>。</p><p><strong>那</strong><code>targetDir</code><strong>是啥?</strong></p><p>在<code>create-vite/src/index.ts</code>的 init 函数里有个<code>getProjectName</code>函数,内容是判断<code>targetDir</code>是否为 '.',是,<code>path.basename(path.resolve())</code>,否,直接用</p><p>首先,我尝试在当前路径<code>C:\disk_D\gyt95</code>执行命令输出看看</p><pre><code class="ts">console.log(path.resolve()); // C:\disk_D\gyt95
console.log(path.basename(path.resolve())); // gyt95</code></pre><p>找官方文档看看描述。</p><h5>path.basename()</h5><p>官方描述:返回路径的最后一部分,类似于<code>Unix</code>的<code>basename</code>命令。忽略尾随目录分隔符。而且区分大小写。</p><pre><code class="ts">path.win32.basename('C:\foo.html', '.html');
// Returns: 'foo'
path.win32.basename('C:\foo.HTML', '.html');
// Returns: 'foo.HTML'</code></pre><p>所以,上面的<code>path.basename(path.resolve())</code>就返回最后一部分,即<code>gyt95</code>。</p><p>那么,<code>path.resolve()</code>是什么?</p><h5>path.resolve()</h5><p>官方描述:将一系列路径或路径段解析为绝对路径。如果没有传递路径段,<code>path.resolve()</code>将返回当前工作目录的绝对路径。</p><p>因此,<code>path.resolve()</code>返回的就是当前工作目录的绝对路径,即<code>C:\disk_D\gyt95</code>。</p><p>知道了获取当前目录路径后,就要进入对应平台名称的目录里创建<code>htmlName</code> ,但不需要真的进行“进入”的操作,因为<code>write</code>本身就有 “进入然后把数据写进去”的意思。(刚开始还一直找有什么函数可以进入某个目录)</p><p>但执行报错,因为不能用<code>cjs</code>格式的<code>require</code>,我写的是<code>require('data.json')</code> <br>解决办法:用<code>Node</code>的<code>fs</code>模块读取当前是否存在这个文件。 <br>根据经验,是有<code>fs.exist</code>的。查了下文档,发现原来<code>Node</code>的<code>v16.19.1</code>已经弃用,建议改用<code>fs.stat</code>或者<code>fs.access</code>。</p><p>当前,我们要在<code>htmlName</code>的目录下创建一个<code>module</code>空目录,已知<code>mkdir</code>创建目录。如何实现?</p><p>其实超级简单!!你直接判断这个路径,<code>fs.ensureDir</code>方法能直接判断你这个路径是否存在,如果不存在,能直接帮你创建这整个路径下的目录。。</p><p>创建完毕后,我直接利用<code>fs.writeFile</code>创建 html 文件!</p><h5>完成 template 动态生成!</h5><pre><code class="ts">const moduleTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${name}</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
`
// ensureDir 可以判断当前平台是否存在 htmlName 是否存在 module 目录
const modulePath = `${platformName}/${htmlName}/module`
try {
await fs.ensureDir(modulePath)
await fs.writeFile(`${modulePath}/${htmlName}.html`, moduleTemplate, 'utf-8')
} catch (error) {
console.log(error)
}</code></pre><p>看看效果</p><p><img src="/img/remote/1460000043589350" alt="" title=""></p><h5>进阶1:动态生成平台选项</h5><p>目前,提供的平台选项是写死的。</p><p>我们希望:平台选项是根据当前目录所有非<code>packages</code>等特殊目录而自动生成的。用一个数组存起来。</p><p>如果数组为空,就提示找不到任何平台,请输入一个。</p><p>如果数组不为空,就展示给用户进行选择。</p><p>此时用户觉得都不是以上的平台,那么就选择最后一个选项“以上都不是”,就会提示请自行输入一个。</p><p><strong>首先,如何获取当前执行 node 的路径下所有目录的名称?</strong></p><p>可以通过<code>fs.readdirSync()</code>方法读取当前路径下的所有目录和文件,然后使用<code>path</code>模块中的<code>path.join()</code>方法来将当前路径和目录名称拼接成一个完整的路径,最后使用<code>fs.stat()</code>方法判断这个路径是否是一个目录。如果是目录,就将它的名称存储到一个数组中。</p><h5>fs.readdirSync</h5><p>官方描述:同步方式读取指定目录下所有文件和子目录名称。</p><p>但以上的思路有个问题:如果遇到<code>.</code>开头的文件,就会报错。可以通过给<code>fs.readdirSync</code>设置<code>withFileType:true</code>获取所有目录和文件的信息,以下是代码和区别</p><pre><code class="ts">const currentNoInfoDirFiles = fs.readdirSync(cwd)
console.log(currentNoInfoDirFiles);
const currentDirFiles = fs.readdirSync(cwd,{ withFileTypes: true })
currentDirFiles.forEach(v => console.log(v, v.isDirectory()));</code></pre><p><img src="/img/remote/1460000043589351" alt="" title=""></p><p>可以看到尽管能通过<code>fs.isDirectory()</code>判断是否为目录,但仍然有一些<code>.</code>开头的目录,需要过滤。并且,还有些特殊项目要过滤。综上,代码最后如下:</p><pre><code class="ts">const BAN = ['packages', 'dist', 'node_modules'] // 这里看你需要自行添加,我的特殊目录就这些
currentDirFiles.forEach(file => {
if(file.isDirectory() && !file.name.startsWith('.') && !BAN.includes(file.name)){
currentPlatforms.push({
name: file.name,
value: file.name
})
}
})
currentPlatforms.push({ name: '其它', value: 'others' })
const { type } = await inquirer.prompt([
{
type: 'list',
name: 'type',
choices: currentPlatforms,
message: '当前项目属于哪个平台的?',
},
])</code></pre><p>当输入新的名字时,我们再做一些处理,避免用户故意写特殊目录的名字。</p><pre><code class="ts">if (type === 'others') {
const { newType } = await inquirer.prompt([
{
type: 'input',
name: 'newType',
message: '新平台的英文名称叫什么?',
},
])
if (!newType) return
fs.ensureDir(newType.toLowerCase())
if(BAN.includes(newType)){
consola.error('不能填写以下名称:packages, dist, node_modules')
return;
}else if(currentPlatforms.includes(newType)){
consola.error('这个名字已经存在!重来!')
return;
}
consola.info(`正在为你创建新的平台目录 ${newType}...`)
consola.info(`为了统一命名规范,会自动转为小写...`)
platformName = newType
} else {
platformName = type
}</code></pre><h5>进阶2:提取 template</h5><p>上文看到我们直接定义变量<code>moduleTemplate</code>。但是更好的办法可以写<code>template.html.tpl</code>。</p><p>原本我也想过这个办法,但是不明白怎么去自定义<code>template</code>的 html 名称,不知道怎么改。</p><p><strong>(不明白就对了,不明白就去了解它!)</strong></p><p>直到后来我看了下<code>Vant</code>的实现方式,因为<code>create-vant-cli-app</code>里的模板包含<code>package.json.tpl</code>。</p><p>因此,我们要从<code>create-vite</code>回来,看回<code>create-vant-cli-app</code>的实现思路。过程中涉及一系列<code>Node</code>的模块的<code>API</code>,会逐一描述。</p><p>主要是<code>writing</code>函数和<code>copyTpl</code>函数,实现原理是这样的:</p><ol><li><strong>首先拿到 templatePath ,具体实现:</strong></li></ol><pre><code class="ts">// this.inputs.vueVersion 的值是用户选择选项 Vue2/Vue3 传入的,对应的值为 vue2/vue3
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
GENERATOR_DIR = path.join(__dirname, '../generators');
const templatePath = path.join(GENERATOR_DIR, this.inputs.vueVersion).replace(
/\/g,
'/'
);</code></pre><h5>path.join</h5><p><strong>作用就是用特定于平台的分隔符将所有传入的路径段落拼接在一起。</strong></p><p>(纯粹拼接。如果参数1是 <code>..</code>, 拼接<code>../generators</code>就变成 <code>....\generators</code> ,如果是 __dirname 即执行文件的目录的绝对路径例如 <code>C:\disk_D</code>,拼接<code>../generators</code>后就会变成<code>C:\disk_D\generators</code>)</p><p><strong>这里还有个关键点!</strong> 虽然我们在代码里写的是 <code>/</code>,但实际上<code>Windows</code>上打印出来的是<code>Windows</code>自己的分隔符<code>`,也就是说,会自动转义。那上文能不能不使用</code>replace`转义呢?</p><p>本来以为不行,但其实是可以的。因为<code>path.join</code>会用对应平台特定的分隔符来拼接。但像<code>create-vant-cli-app</code>这里添加了<code>replace</code>是因为之前有 issue 说 <code>Windows10</code> 系统下执行<code>yarn create vant-cli-app</code> 后只出现一个<code>node_modules</code>。所以还是建议<code>replace</code>一下。</p><p>根据上文提到的 <code> __dirname </code>作用可知,实际上对应的是 <code>lib/index.js</code> ,那么 <code>../generators</code> ,刚好就是和 <code>lib</code> 目录同级的 <code>generators</code> 。</p><p><img src="/img/remote/1460000043589352" alt="" title=""></p><p>这里用<code>replace</code>方法把<code>\</code>都改为<code>/</code>。(注意:这里<code>\\</code>第一个<code>\</code>是<code>JS</code>的转义字符,下文立刻会说)</p><p>因此,用户选择<code>Vue3</code>时,获取路径是<code>to/path/generators/vue3</code> 。而 <code>to/path</code> 是绝对路径。</p><p>可以用<code>fileURLToPath</code>解决。</p><h5>路径分隔符 \ 和 / 的区别?</h5><p>都是路径分隔符。</p><p>在 <code>Windows</code> 操作系统中,路径分隔符通常是反斜杠(\),而在 <code>Unix</code>和<code>类 Unix</code> 操作系统(如 <code>Linux</code> 和 <code>macOS</code>)中,路径分隔符通常是正斜杠(/)。</p><p>在<code>JavaScript</code>中,反斜杠也可以用作转义字符。因此,如果你在 <code>Windows</code> 操作系统上编写<code>JavaScript</code> 代码并使用反斜杠作为路径分隔符,那么在某些情况下可能需要将路径中的反斜杠字符替换为斜杠字符,以确保正确解析路径。</p><ol start="2"><li><strong>接着拿到 templateFiles ,具体实现:</strong></li></ol><pre><code class="ts">const templateFiles = glob.sync(
join(templatePath, '**', '*').replace(/\/g, '/'),
{
dot: true,
}
);</code></pre><p>刚才得到了 <code>vue3</code> 模板的绝对路径,现在通过 <code>path.join</code> 拼接出 <code>to/path/generators/vue3/**/*</code> 路径(是的,<code>path.join</code> 会自动加分隔符)</p><p>这里提到了 <code>glob</code> 。用的是 <code>fast-glob</code> 这个库,那么 glob 是什么?</p><h5>glob</h5><p>通过星号等 <code>shell</code> 所用的模式匹配文件。它一般被用来查找指定目录下的所有文件和子目录<code>glob.sync</code>只是同步方式。</p><p>早期<code>Unix</code>(第 1-6 版,1969-1975)的命令行解释器依赖独立程序<code>/etc/glob</code>展开参数中的通配符。这个程序会展开通配符并把展开后的文件列表传给命令。它的名字是 "global command" 的简称。后来这个功能由工具函数 glob() 提供,被<code>shell</code> 等程序使用。(译自 <a href="https://link.segmentfault.com/?enc=ZjTX56P%2FPMxK1fqIG4zONw%3D%3D.A%2FiVtb1FacGo%2FC1%2BFq1WRMZCmtPoFcRZKO4QfTdmSwu82M6u7IK6%2Bmcu%2BjnbJMTb1miOTZLW78mm8JfC17moiso9C3V2jbCO9XADi2mb%2BB%2FJwZRh1ebZKOXoZIJ8Z1EB" rel="nofollow">WikiPedia</a>%23Origin))</p><p>(再次和<code>Unix</code>相关的知识点联系上)</p><p>其实就像这样的模式:用在命令行中的<code>ls *.js</code>,用在 <code>.gitignore</code> 文件中的 <code>build/*</code>。</p><p><strong>所有用到 * 的路径可以用 glob 进行匹配。</strong></p><p>这里有个参数<code>{ dot: true }</code>,它是<code>glob</code>库中的一个选项,用于匹配隐藏文件(以 . 开头的文件)。如果将 dot 设置为 true,则<code>glob.sync()</code>将会匹配隐藏文件,否则将会忽略这些文件。(而这里因为<code>create-vant-cli-app</code>的<code>vue2/vue3</code>模板里都有<code>.gitignore</code> <code>.eslintignore</code>这样的以点号<code>.</code>开头的文件,所以要带上这个属性)</p><p>在上面的代码中,<code>dot: true</code> 表示匹配所有文件,包括隐藏文件。</p><ol start="3"><li><strong>遍历 templateFiles</strong></li></ol><pre><code class="ts">templateFiles.forEach((filePath) => {
const outputPath = filePath
.replace('.tpl', '')
.replace(templatePath, this.outputDir);
this.copyTpl(filePath, outputPath, this.inputs);
});</code></pre><p>每个路径都把<code>.tpl</code>字眼<code>replace</code>为空字符,再替换<code>templatePath</code>为输出目录。然后调用<code>copyTpl</code>方法,传入3个值: <code>filePath</code>每个模板文件路径,<code>outputPath</code>输出路径,<code>this.inputs</code>用户输入的所有信息集合。最后这个<code>this.inputs</code>是<code>copyTpl</code>方法要根据用户输入的信息,对<code>.tpl</code>文件进行内容动态修改。</p><p><strong>为什么要替换为输出路径?</strong></p><p>目的是将模板文件的路径替换为在新项目中的相应路径。这是为了确保生成的文件被正确地放置在新项目的相应位置上,并避免任何文件名冲突。</p><p>看下注释版</p><pre><code class="ts">// 已知: outputDir 是 C:\aaa\username\projectName
// 已知: templatePath 是 C:\xxx\vant\create-vant-cli-app\generators\vue3
templateFiles.forEach((filePath) => {
// filePath 是 C:\xxx\vant\create-vant-cli-app\generators\vue3\package.json.tpl
const outputPath = filePath
.replace('.tpl', '')
// 变成:C:\xxx\vant\create-vant-cli-app\generators\vue3\package.json
.replace(templatePath, this.outputDir);
// 变成:C:\aaa\username\projectName\package.json'
this.copyTpl(filePath, outputPath, this.inputs);
});</code></pre><ol start="4"><li><strong>执行 copyTpl 方法</strong></li></ol><p>已知,每个模板文件都会调用一次这个方法。那么以<code>create-vant-cli-app</code>为例,看看内部实现:</p><ol><li>通过<code>fs.copySync</code>把源路径文件,复制到,目标路径。即输出的目录里</li><li>通过<code>fs.readFileSync</code>读取输出路径下文件内容</li><li>遍历用户输入的集合<code>this.inputs</code>,通过正则表达式,查找是否有对应的模板语法 <%= ${key} %> ,有,替换为 key为name 所对应的 value</li><li>遍历完毕就通过 fs.writeFileSync 把新的内容写入到输出文件里</li></ol><pre><code class="ts">function copyTpl(from: string, to: string, outputDir: string, args: Inputs) {
// 4-5-1 复制文件
fs.copySync(from, to)
// 4-5-2 读取文件
let content = fs.readFileSync(from, 'utf-8') // utf-8 是为了获取非 buffer 类型数据
// 4-5-2 遍历,替换掉模板语法
Object.keys(args).forEach(key => {
// 在正则表达式中,'g'代表全局匹配模式,表示匹配字符串中所有符合条件的子串
const reg = new RegExp(`<%= ${key} %>`, 'g')
content = content.replace(reg, args[key as keyof Inputs])
})
// 4-5-3 写回输出目录的对应文件
fs.writeFileSync(to, content)
// 4-5-4 动态改名
if (path.basename(to) === 'template.html') {
const newToPath = to.replace('template.html', `${args.htmlName}.html`)
fs.renameSync(to, newToPath)
}
// 4-5-5 提示成功
// 把目标路径的前面部分和平台分隔符去掉,剩下的就是文件名了
const name = to.replace(outputDir + path.sep, '')
consola.success(`${color.green('创建')} ${name}`)
}</code></pre><p>注意:第11行有一个<code>g</code>,不要误以为是字符了。。这在正则表达式里表示开启全局匹配模式。</p><h5>fs.writeFileSync</h5><p>当 file 是文件名时,同步将数据写入文件,如果文件已存在则替换该文件。数据可以是字符串或缓冲区。</p><p>当 file 是文件描述符时,其行为类似于直接调用 <code>fs.write()</code>(推荐)</p><p>参数介绍:</p><ol><li>path:要写入的文件的路径(必需参数)。</li><li>data:要写入到文件中的数据(必需参数)。</li><li>options:一个可选的选项对象,用于指定文件的编码、文件模式、文件权限等(可选参数)。</li><li>encoding:一个可选的编码字符串,用于指定写入文件时使用的编码格式(可选参数)。如果省略此参数,则默认使用UTF-8编码。</li></ol><p><strong>这里包含动态修改 template.html.tpl 名称。</strong> (<code>create-vant-cli-app</code>没有这一步)</p><p>由于模板的名称叫<code>template.html.tpl</code>,但具体项目的 html 应该是对应<code>projectName</code>的。</p><p>这里可以用<code>fs</code>模块的一个方法<code>renameSync</code>。</p><h5>fs.renameSync</h5><p>官方描述:将文件从 oldPath 重命名为 newPath。返回 undefined 。</p><pre><code class="ts">if(path.basename(to) === 'template.html'){
const newToPath = to.replace('template.html', `${htmlName}.html`)
fs.renameSync(to, newToPath)
}</code></pre><p>到了这里,才完成读取-遍历-写入,3个步骤。</p><ol start="5"><li><strong>附加写入成功提示,这里用到了 path.sep</strong></li></ol><h5>path.sep</h5><p>官方描述:提供特定于平台的路径段分隔符。例如<code>Windows</code>下是这样的:</p><pre><code class="bash">'foo\bar\baz'.split(path.sep);
// Returns: ['foo', 'bar', 'baz']</code></pre><p>在<code>Windows</code>上,正斜杠 (/) 和反斜杠 () 都被接受为路径段分隔符;但是,路径方法只添加反斜杠 ()。</p><p>以下是具体代码👇</p><pre><code class="ts">copyTpl(from: string, to: string, args: Record<string, any>) {
fs.copySync(from, to);
let content = fs.readFileSync(to, 'utf-8');
Object.keys(args).forEach((key) => {
const regexp = new RegExp(`<%= ${key} %>`, 'g');
content = content.replace(regexp, args[key]);
});
fs.writeFileSync(to, content);
const name = to.replace(this.outputDir + sep, '');
consola.success(`${color.green('create')} ${name}`);
}</code></pre><p>小结:<code>copyTpl</code>方法作用是复制模板里的文件到指定目录。其中包含类似于模板引擎的语法替换。把<code>.tpl</code>文件里的<code><$= key $></code>替换为对应的值。</p><p>有了上面的步骤,直接就生成剩下的文件了,包括<code>package.json</code>。</p><h4>阶段3:内部自动生成目录及文件</h4><p>其实就是上面的源码解析实现一次。</p><h2>第三个小目标:制作欢迎界面</h2><p>这部分没有点创意是不行的。让 ChatGPT 帮我想办法,结果做出来的欢迎界面至少我个人挺喜欢的。</p><pre><code class="ts">import color from 'picocolors'
import boxen, { Options } from 'boxen'
import gradientString from 'gradient-string'
// 终极版
const welcomeMessage = gradientString('cyan', 'magenta').multiline(
['Hello! 欢迎使用EKTFE脚手架~', '😀🎉🚀'].join('')
)
const boxenOptions: Options = {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
backgroundColor: '#000',
}
console.log(boxen(welcomeMessage, boxenOptions))</code></pre><p>具体的话,去<code>GitHub</code>搜下对应的库就好。</p><p>效果如图:</p><p><img src="/img/remote/1460000043589353" alt="" title=""></p><h2>第四个小目标:发布并用于实践</h2><h4>是否要发布 npm 包?</h4><p>我们公司是没有私有服务器的。如果要发布,就只能发布到外网 npm 上。那假如不发布到 npm ,其实也不是不行。就只要让对方在<code>create-cli-app</code>执行<code>yarn link</code>,然后在根目录执行<code>yarn create xxx</code>即可。这种做法,对于网络不好的时候,这是个好办法。</p><p>最后我选择发布到外网,反正也不是什么商业机密。像之前提到的“平台选项”,不写死,没啥问题。可以说是通用的了。</p><p>而且发布到外网,使用起来比本地使用要方便,至少少了一步<code>yarn link</code>。</p><p>主要的发布流程:(估计都被说烂了。但其实开发脚手架也是被说烂了,但我还是写了)</p><p>你只要注册 npm 账号,再通过<code>yarn login</code>和<code>yarn publish</code>就可以发布了。(或者用<code>npm login</code>和<code>npm publish</code>)</p><h2>后续优化</h2><p>1.当出现多个模版时,需要创建多个<code>template</code>,并提供给用户进行选择。</p><p>2.某些项目可能从一开始就要安装一些库,例如<code>echarts</code>。后期可以增加一个列表选项进行多选添加,这里估计又是知识盲区了。前期其实基于根目录的依赖项就行。</p><p>3.目前<code>containers</code>目录下的子目录名称等还没有变成自动化,不过方式和前文提到的<code>template</code>类似。可以后期再写。</p><p>4.代码层面的优化。</p><h2>其它问题</h2><h3>.eslintrc.js 继承外部导致路径错误?</h3><pre><code class="js">module.exports = {
extends: ['../../.eslintrc.js'],
}</code></pre><p>原本打算通过<code>lint-staged .eslintignore</code>这类方式来跳过对<code>template</code>的<code>eslint</code>检测。但发现不行。</p><p>最后想了个办法,直接把<code>.eslintrc.js</code>更名为<code>.eslintrc.js.tpl</code>就失去了它原本的作用了,复制文件时会去掉<code>.tpl</code>的后缀,就又恢复了。</p><h3>打包构建之前如何清理 lib 目录?</h3><p>直接用<code>rimarf</code>不行。</p><h4>rimraf 是什么?</h4><p>一个用于递归删除文件和文件夹的 npm 包。在<code>Windows</code>上,由于文件夹路径的斜杠方向与<code>Unix</code>系统不同,因此<code>rimraf</code>会出现一些问题。</p><p>可以使用<code>cross-env</code>这个库,可以跨平台地设置环境变量。</p><pre><code class="json">"build": "cross-env rimraf lib && tsc"</code></pre><p>也可以使用<code>del</code>这个库。</p><pre><code class="json">"build": "del lib && tsc"</code></pre><h3>如何用命令行创建LICENSE?</h3><p>发现还没有许可证,发现网上大多数是可视化创建,我想用命令行,<a href="https://link.segmentfault.com/?enc=3ufADN9nslqk%2Fs8SNvQLSQ%3D%3D.611mjurQoMLvL23yvqdnga0Z3QSNUwLVkXN9kbojdecmS17Fc2V2VCRGwweW0Or1" rel="nofollow">license-generator</a>这个库可以解决。</p><h2>小结</h2><p>这篇文章是随着我阅读源码到开发脚手架整个过程一直写下来的,过程中看过<code>Vant</code>的源码、<code>Node</code>文档、<code>Yarn</code> 文档,以及各种库的文档,后来因为开始用 ChatGPT ,就决定试试在为我的日常工作学习提供一些帮助。</p><p>每天花一些时间开发脚手架、涉及的各种细节还要做笔记(因为几乎全是本人知识盲区)。不得不说涉及的知识点还是挺多。用了一周时间终于完成。再用近一周的时间基于业务进行优化,并且要重新整理文章。整理文章这个过程耗时很久,一是因为一直做笔记下来,后面的理解有时推翻前面的,所以前面可能存在错误的认知还有一些多余的推测,所以需要重看自己之前写的内容,很多不需要的就去掉,还有很多排版等问题。另外,为了可读性补上行内代码的样式,体力活,我还是喜欢直接空格多一些,写起来舒服,但不利于阅读。</p><p>原标题其实是“关于我编写一个脚手架顺便看完几个开源库源码的事”,但无奈就以上这点内容搞了很久,所以只能后续再研究。</p><h3>收获</h3><ol><li>对<code>yarn</code>的一些命令、<code>package</code>的安装和执行、<code>package.json</code>各种字段、脚手架的基本实现原理,都有较深刻的认识;</li><li>把<code>create-vant-cli-app</code>项目源码读完,虽然代码量超级少。但细节还是有的,主要是复习了不少<code>Node</code>常用的知识点(同理,可以把<code>create-vite</code>等等也尝试拿下);</li><li>输出了一个基于业务的定制化脚手架。通用脚手架多的是,基于业务才是重点;</li><li>有趣的是,因为是定制化,所以提示语可以写直白点。如果是开源的通用脚手架,就要正式口吻;</li><li>后期使用了 ChatGPT 解决了一小部分问题,它的好处是对于一些不太难但找起来比较麻烦的知识点可以很快给出一个思路。如果遇到一些问题卡住了,去询问它比你查资料要快,然后比如说它抛出了一个 API ,我就会去文档里找,确实快了不少。部分代码优化也可以借助它。不过它也经常会一本正经胡说八道,所以如果没训练到位的话,注意不要被它忽悠了。</li></ol><h3>NEXT</h3><p>先看下<code>create-vite</code>和<code>create-vue</code>等源码。然后开始看<code>vant-cli</code>和<code>vite</code>。</p>
所有用CSS3写的3D特效,都离不开这些知识
https://segmentfault.com/a/1190000010385822
2017-07-28T01:24:50+08:00
2017-07-28T01:24:50+08:00
gyt95
https://segmentfault.com/u/gyt95
2
<h2>起因</h2>
<p>昨晚在做慕课网的十天精通CSS3课程,其中的综合练习是要做一个3D导航翻转的效果。非常高大上。</p>
<p>以往这些效果我都很不屑,觉得网上一大堆这些特效的代码,复制粘贴就好了,够快。但是现实工作中,其实自己写出来,比你网上找代码要快很多,因为你是不会才去找代码粘过来的。那么你就要去看哪些代码需要用,哪些不需要。而如果是自己写的话,哪里漏了什么,再去查,明显快些,如果很熟练,写得就更快了。</p>
<p>这些常见特效真要让我自己写出来,竟然束手无策。坐在电脑前开始怀疑之前学的前面几章节的CSS3包括以往学的CSS3知识都是什么鬼,自己没能力写出这个效果我有啥资格不屑这些特效呢?然后参考了下答案,发现单纯做完上面的CSS3基础题,是完成不了这个练习的。换言之,就像FCC的个别综合题一样,你需要自己去查一下其他知识并应用起来,才能完成。</p>
<p>看了下,一大堆兼容前缀,还有几个陌生的属性:perspective是什么?transform-style?preserve-3d?translateZ??</p>
<p>简直黑人问号脸,亏我还自认为对CSS3很熟悉了,以为只要会用transform的4种变换和transition就足够了。。</p>
<p>难怪7月初面试前端,面试官问我CSS3的知识时我感觉自己的回答是在CSS3的边缘行走。。。</p>
<p>然后就去找啊。。之前张鑫旭博客写的loading效果讲解得不错啊,既然是CSS大神,应该能搜出点什么,结果一搜perspective,还真有。而且其他的属性全都提到。</p>
<p>于是。。原本是打算做导航3D翻转效果的,看文章看得起劲,做了个效果凑合的3D旋转木马出来。。。以前的我也是觉得这个效果好屌,好难,看完文章发现,难度还能接受。</p>
<p>其实有犹豫过还要不要写文章来总结,大神已经写了这么有趣这么好的文章了,我再写不是浪费时间吗。。而且现在还哪有人写个关于特效的文章啊。。但我还是写了。。作为今天学习的一个总结也好。还有,这篇文章没有教你写任何一个具体的特效。</p>
<p>假如喜欢大神的有趣讲解,可以点击<a href="https://link.segmentfault.com/?enc=KkEIldUbcC0yFA81CJG2aQ%3D%3D.Vetn8m7tHIX8C6TIYV7bFMW1YZSROQhb9nRYTVaz00I%2B61TSkjURklG0J2HMTm6%2FjzcXfauysJlUxTwVRWprcUUg1Dy4vO6tKV2b2vAwtAkllVxq50BiT61Igr8Zv7W6" rel="nofollow">这里</a>。</p>
<p>本文就不废话了,直接开始。("废话已经够多了好吗!")</p>
<h2>涉及到的知识点</h2>
<p>rotateX rotateY rotateZ<br>translateZ<br>perspective<br>transform-style: prserve-3d</p>
<h2>rotateX rotateY rotateZ</h2>
<p>学别人写3D特效,首先你得要有3D概念啊!</p>
<p>何为3D,3D就是立体。是几何概念。</p>
<p>虽然数学是我的弱项,空间思维也不强,但反复思考,还是能弄懂的。</p>
<p>港真,尽管大神生动地为rotateX rotateY rotateZ 3种属性各举一例,然而我就是没懂rotateZ,好尴尬。。飞刀特技表演和把妹子抱床上侧躺。。。我还是无法理解。。。</p>
<p>如果你能理解,就可以跳过下面那些直接到下一个讲解。<br>如果和我一样有点懵逼,你可以看下下面那幅图。请无视我的画工。</p>
<p><img src="/img/bVRJYW?w=1152&h=711" alt="clipboard.png" title="clipboard.png"></p>
<p>如果你还是不懂,不怕,那就听听我的故事吧。</p>
<p>当时,我开始有点急躁,怀疑人生了,看到桌上一支笔。终于懂了,上天还是会可怜一下笨蛋的。</p>
<p>把笔横向拿着,拿出食指围绕它转圈,这就是rotateX<br>把笔竖起来拿着,拿出食指围绕它转圈,这就是rotateY</p>
<p>最让我困惑的就是rotateZ,其实就是你把笔指向自己(当然你不指向自己指向对面也行。。),然后同样地拿出食指围绕它转圈。这我才明白飞镖和妹子侧躺那张图的意思(哎哟,这智商。。)</p>
<h2>好像很难的perspective属性</h2>
<p>3D变换的第一个重点知识。</p>
<p>perspective即望远镜,透视的。</p>
<p>这个属性刚开始接触,觉得好深奥,太抽象了。</p>
<p>那就结合demo来看,假设:</p>
<p>背景色为白色的是父元素,背景色为黄色的是子元素<br>在父元素上设置perspective为100<br>对子元素设置45度正向翻转rotate(45deg)</p>
<p>效果如图:</p>
<p><img src="/img/bVRJYX?w=361&h=304" alt="clipboard.png" title="clipboard.png"></p>
<p>然后我把perspective调大,改成300,效果如图:</p>
<p><img src="/img/bVRJYY?w=294&h=297" alt="clipboard.png" title="clipboard.png"></p>
<p>结论:<br>perspective取值越小,3D效果就越明显,也就是你的眼睛越靠近真3D。</p>
<p>因此,perspective你可以理解为视距。</p>
<h2>translateZ属性</h2>
<p>现在我们假设perspective是固定的,50px。</p>
<p>我们通过设置不同的translateZ,来看看结合着理解。</p>
<p>html代码:</p>
<p><img src="/img/bVRJY1?w=318&h=238" alt="clipboard.png" title="clipboard.png"></p>
<p>css代码:</p>
<p><img src="/img/bVRJY3?w=416&h=597" alt="clipboard.png" title="clipboard.png"></p>
<p>效果:</p>
<p><img src="/img/bVRJY4?w=1529&h=872" alt="clipboard.png" title="clipboard.png"></p>
<p>我们可以发现,translateZ越大,该元素离我们眼睛越近,当其大于等于perspective时,就会从肉眼消失。</p>
<p>这里要注意perspective所在位置,即书写方法。</p>
<p>1)写在舞台元素中(即父元素):就是上面我们写的那种<br>2)写在子元素中:transform( perspective(50px), translateZ(30px))</p>
<p>两种写法区别在于子元素是否拿同一个东西作为参照物,是的话,改变perspective这个大神那篇文章写得很仔细了,这里就省略了。</p>
<h2>简单却重要的transform-style属性</h2>
<p>为什么说简单,你看它语法。。就两个值。。</p>
<p>transform-style: flat | preserve-3d</p>
<p>为什么说重要,因为它默认值是flat。意味着该元素的所有子元素不具备3D效果,你加了什么perspective,加了很复杂很华丽的transform都没用,设置的是flat值,就全都得变2D,所有子元素都只能以平展形式呈现在眼前,什么?你想要看怎么个平展法?</p>
<p>好吧,那下面我就通过实例让你们知道这个transform-style属性的厉害。</p>
<p>首先是旋转木马原本效果。</p>
<p><img src="/img/bVRJY5?w=499&h=364" alt="clipboard.png" title="clipboard.png"></p>
<p>然后去掉transform-style: preserve-3d,接下来的画面可能会引起情绪不安,请在家长陪同观看。</p>
<p><img src="/img/bVRJY7?w=575&h=342" alt="clipboard.png" title="clipboard.png"></p>
<p>"哇!好丑!"</p>
<p>所以说这个属性,简单但很重要。不要忘加了。</p>
<p>PS:不能为了防止子元素溢出容器而设置overflow值为hidden啊,如果设置了overflow:hidden同样会导致子元素出现在同一平面(和元素设置了transform-style为flat一样的效果)。</p>
<h2>尾声</h2>
<p>写到这里,3D变换常用的属性也说完了,属性很少,难就难在比较抽象,需要花点时间理解。其实还有些属性没有提到,例如透视属性backface-visibility:hidden,设置为hidden则无法看穿了。</p>
<p>第一次一口气写这么长的一篇文章,其实一开始我是拒绝的,在前端工程化、各种前端开发框架盛行的现在,我觉得已经没有多少人再去研究CSS3这方面知识了,但很多人却都在简历上说自己精通CSS3(当然,我的简历没有这么写哈),平时就算遇到特效,没有网上找代码,他们大多也是从自己<strong>整理</strong>好的demo库里找出来复制粘贴(注意,不是写好,是整理好的而已),但是想想一天下来,从看文章理解,到自己写特效,再到总结知识点,梳理3D变换套路。。。</p>
<p>折腾了这么久,不写点东西感觉对不起自己啊,虽然确实有点累,但收获还是挺多的,起码不会再怕3D变换了,我知道CSS3水很深,3D变换也是,很多坑需要在写特效时才会遇到。</p>
<p>突然有点忘记刚刚自己写了啥了,那顺便写个小结吧。</p>
<p>首先是perspective,视距,这个属性要写在父元素上,设置用户和元素3D空间的Z平面距离。视距perspective越小,3D效果越明显,肉眼离Z屏面距离越近。</p>
<p>然后就是translateZ,值越大,证明越靠近你的眼睛。当超过了perspective设置的值时就会消失,它只是太大,大到你看不见而已。</p>
<p>最后就是要在父元素的子元素中设置transform-style:preserve-3d,表明子元素需要用到3D空间,不设置的话如何3D变换也会变为2D平展。</p>
<h2>参考</h2>
<p>感谢张鑫旭这篇文章:<br><a href="https://link.segmentfault.com/?enc=1WrsQeWVg8aO1M1yto0UrQ%3D%3D.s6piFZWSL03cbQSbu0ruHXGZ5yVDeVeyxc%2FM%2FT1%2FQ8JNClewJnZvUOcMikApwYna2J5WATs2%2BFcKfiNj1jf9VGcbFuqJdC%2BOcovg2EGWjMKBYIfnItdEI8RjA%2BQD2jec" rel="nofollow">好吧,CSS3 3D transform变换,不过如此!</a><br>还有一篇写得不错的,帮助理解:<br><a href="https://link.segmentfault.com/?enc=eB86pKEWEdAlpcNfdDpHdg%3D%3D.tS3mwZwGH89Fe5MQkskRr%2BQ6u%2BWPz6DOBKBI8fehJO4ZOt%2F1ktLFeqZ1Vy8h3Q2sVmBPil%2BNLT5vOGq2gl0yOg%3D%3D" rel="nofollow">Transform-style和Perspective属性</a></p>
<p>最后引用张鑫旭的一句话:</p>
<blockquote><p>纯粹从网上copy些效果代码,那永远就是copy的命咯!</p></blockquote>
一个简单的loading效果的知识总结
https://segmentfault.com/a/1190000010370761
2017-07-27T10:11:53+08:00
2017-07-27T10:11:53+08:00
gyt95
https://segmentfault.com/u/gyt95
0
<h2>起因</h2>
<p>昨晚在慕课网的十天精通CSS3课程中,看到animation那一块时,突然想去张鑫旭博客看看他有没写一些用animation做的有趣的动画。结果看到了一个我之前很想做,但不知道怎么实现的效果。</p>
<p>这个效果描述起来有点难度。就是平时页面加载数据的时候可能会显示“加载中...”,我想实现的就是后面的那个“...”,如何能让他逐个显示,然后循环。</p>
<p>学了animation后,这个还是很好实现的。但张鑫旭的效果则多了加载时间太久时,会显示“超时”的提示语。关键代码如图,详细代码在<a href="https://link.segmentfault.com/?enc=n7v25VK1j0kah3iu5mTkHA%3D%3D.AqZlvwA1yNIC%2BbsUtjfdE8D7iNOd%2BvoqhKWBmoY7sNa9UIzWyjv%2BXAcjm0R7mRki8UT%2F8v%2FZ3PGlR9tUerBte5z03dOKkBiTENGAfDoK3O4%3D" rel="nofollow">这里</a>。</p>
<p><img src="/img/bVRF3u?w=687&h=345" alt="clipboard.png" title="clipboard.png"></p>
<p>这个很人性化啊。js代码也很少。但问题来了,就几行代码,我却没看懂。。。<br>1.为什么用bind,不是都用on的吗?bind和on有啥区别<br>2.setTimeout()为什么绑定的函数后面要用.bind(this)</p>
<h2>jQuery中bind和on的区别</h2>
<p>都是用于绑定事件。如果绑定的元素已存在,那么用哪个都可以,没有区别。</p>
<p>注意了,是在所绑定的这个元素已经存在的前提下,两者无区别。<br>那如果这个元素原本是没有的,需要点击一个按钮,才动态生成呢?</p>
<p>我们看下jQuery文档中bind和on绑定事件的语法:</p>
<blockquote>
<p>.bind(events [,eventData], handler)</p>
<p>.on(events [,selector] [,data], handler)</p>
</blockquote>
<p>从语法中可以看到,.on方法比bind多了一个参数'selector'。</p>
<p>其作用是筛选出调用.on方法的dom元素的指定子元素,最常用的情景就是,一个ul列表,我需要点击“添加li”按钮后,在ul列表最后增加一个li。如下:<br>$('ul').on('click','li',function(){console.log('click');});</p>
<p>因此,.on方法的好处就在于当我用</p>
<pre><code>$('ul').append('<li>我是新增的li</li>');</code></pre>
<p>来动态生成新的li时,.on方法能对其这个新生成的元素绑上click事件。而.bind方法则不行,因为它的语法中没有提供'selector'这个参数。</p>
<p>.on方法的这个好处就是传说中的事件委托。将对某子元素的事件委托给其父元素,当点击子元素时,由于事件冒泡原理,会向上一级(即父元素)冒泡,就触发了我们用.on方法绑定在父元素的事件。</p>
<h2>setTimeout中的this指向</h2>
<p>因为感觉setTimeout里面有很多坑,所以对setTimeout很陌生。<br>一查才知道setTimeout传入函数时,函数中的this指向window对象。如下:</p>
<pre><code>var num = 0;
function Obj (){
this.num = 1,
this.getNum = function(){
console.log(this.num);
},
this.getNumLater = function(){
setTimeout(function(){
console.log(this.num);
}, 1000)
}
}
var obj = new Obj;
obj.getNum();//1 打印的是obj.num,值为1
obj.getNumLater()//0 打印的是window.num,值为0
</code></pre>
<p>可以看到setTimeout中函数内的this是指向了window对象。因为setTimeout()调用的代码运行在与所在函数完全分离的执行环境上,导致这些代码中包含的 this 关键字会指向 window (或全局)对象。详细可以看<a href="https://link.segmentfault.com/?enc=MJsO9VxkH3cps50ABwD6bw%3D%3D.6n%2FSlJ4j56Xz9W6Rq%2F3J6BkxClJg%2FZQ0KGcpR%2B0RpmywbNO%2B7%2FUOxfmfUBy%2F6ra6LYLbBR%2B1EjbeNwkYxJJtZGYVzr%2BUPdPW10XAYJ59X6M%3D" rel="nofollow">MDN setTimeout</a>。</p>
<h2>用bind纠正setTimeout中this指向</h2>
<p>刚开始我以为这个bind和前面提到的bind是同一个bind。后来看了下<a href="https://link.segmentfault.com/?enc=u7vIsWn1s3b6UZ1u8eqDIQ%3D%3D.JKmXISREaRtbZxlZm%2BisYkuVVgdVx6eawGre4VI2PZObREFT3NrwUH%2B6KeRJY6SDn5L60wDDtsD3D0WK8zof3a6l0%2BF5GP4DjG5PaarIpoVWeDmBy8EvPFPYHH0UL%2BpU" rel="nofollow">MDN bind</a>才发现是不同的。</p>
<p>它是Function.prototype的一个内置方法,之前提到的bind是jquery绑定事件的一个方法。</p>
<p>bind语法:bind(this.args,arg1[, arg2[, ...]]])<br>后面那段参数可能看得有点晕,没关系,现在只要用第一个参数就好了。</p>
<p>看回张鑫旭那段代码中setTimeout写法</p>
<pre><code>setTimeout(function() {
this.ajaxing = false;
this.innerHTML = "提交超时";
}.bind(this), 30000);</code></pre>
<p>首先要清楚当前的this指的是所要点击的a元素。</p>
<p>当setTimeout中的函数执行时,bind会创建一个新函数,bind的第一个参数会作为新函数运行时的this指向,所以原本指向window的this就改为指向了当前这个this,即指向了这个a元素。</p>
<h2>其他方法</h2>
<p>改变setTimeout中this指向还有两种方法。一种是闭包,将this存到一个变量中,setTimeout中函数内部访问这个新的变量,就能得到当前的对象。改写一下上面的代码,如下:</p>
<pre><code>$("#submit").bind("click", function() {
if (!this.ajaxing) {
var that = this;
this.ajaxing = true;
this.innerHTML = '提交订单中<span class="ani_dot">...</span>';
setTimeout(function() {
that.ajaxing = false;
that.innerHTML = "提交超时";
}, 30000);
}
});
</code></pre>
<p>还有一种方法是用箭头函数,直接改写下setTimeout即可</p>
<pre><code>setTimeout(()=>{
that.ajaxing = false;
that.innerHTML = "提交超时";
}, 30000);
</code></pre>
<h2>参考</h2>
<p><a href="https://link.segmentfault.com/?enc=DBRLGzJjJ24RIDsrmt8eRw%3D%3D.53T9W4KEZH950WuRRDFUwpJ8deHtZKFw%2FZXebFrnfqsqkGuQjL%2B0YXyFeFE5Ku%2Bb" rel="nofollow">jquery的bind跟on绑定事件的区别</a><br><a href="https://link.segmentfault.com/?enc=LWkF2cULzGd3MeqDguhHVg%3D%3D.DvE3ZKyAwDUfhzrxLhBfiQBUQFuq1YVRAIlJuiFfSKOWEAQC4uUNLavHMF8ixi0Q" rel="nofollow">关于setInterval和setTImeout中的this指向问题</a></p>
关于跨域和jsonp的一些理解(新手向)
https://segmentfault.com/a/1190000009577990
2017-05-27T12:28:12+08:00
2017-05-27T12:28:12+08:00
gyt95
https://segmentfault.com/u/gyt95
7
<p>非常惭愧,还记得2016年那人生中第一次面试,被问到有没用过ajax?你怎么解决跨域问题时,我回答没用过。。不知道的时候,面试官那一脸茫然,对于当时以为js只是做个轮播图,做点小动画的我来说,ajax、跨域什么的就如同一道难以逾越的高墙一般。。</p>
<p>在后来的实习中终于接触到并且运用起了ajax,跨域也解决过,不过是后端同事解决的(现在才知道那应该就是CORS了吧),所以jsonp到底是啥,每次搜索完看了一下就放弃了。</p>
<p>本文内容浅显,适合人群:<br>1.不懂什么是跨域<br>2.不想自己弄两个不同域名进行跨域测试的童鞋</p>
<h2>什么是跨域</h2>
<p>我的理解是,当用户对不同协议或不同端口或不同域名的资源进行访问时,就是跨域。</p>
<h2>为什么会造成跨域</h2>
<p>罪魁祸首:同源策略</p>
<p>同源定义:即同一域,即相同协议&相同端口&相同域名&相同子域名</p>
<p>同源策略规定:XHR对象只能访问与包含它的页面位于同一域中的资源,有利于预防一些恶意行为。</p>
<h2>怎么解决跨域</h2>
<p>解决办法有很多,CORS、iframe、h5新特性postMessage等,而比较简单的方法就是今天着重介绍的jsonp。</p>
<p>解决依据:尽管不能访问非本域的动态资源,但是类似js文件、样式、图片等静态资源是可以访问的!就是通过这个“漏洞”来解决跨域问题。用<script>标签中的src来写入跨域数据的url,这样就能绕过同源策略了。。“老师,他作弊!”</p>
<h2>简单介绍jsonp</h2>
<p>JSONP,JSON with Padding的简写,这个全称对jsonp的理解还是有一定的帮助的。填充式JSON或者说是参数式JSON。JSONP的语法和JSON很像,简单来说就是在JSON外部用一个函数包裹着。JSONP基本语法如下:</p>
<p>callback({ "name": "kwan" , "msg": "获取成功" });</p>
<p>JSONP两部分组成:回调函数和里面的数据。回调函数是当响应到来时,应该在页面中调用的函数,一般是在发送过去的请求中指定。</p>
<p>JSONP原理:<br>刚才的解决依据可知,JSONP原理就是动态插入带有跨域url的<script>标签,然后调用回调函数,把我们需要的json数据作为参数传入,通过一些逻辑把数据显示在页面上。</p>
<h2>原理看千遍,一写又不会</h2>
<p>我想说这个JSONP我看了很多次,参考过不同的文章,原理都会背了,结果自己写一个出来,傻眼。。</p>
<p>网上有很多文章,良心一点的会配有些代码解释,然而对小白来说,要自己down个WAMP什么的服务器集成软件或者自己搭个服务器,再弄两个不同源的html才能进行模拟跨域,这真的。。。。好麻烦。。</p>
<p>终于,看到了一篇文章,一个通过调用api接口来模拟跨域请求数据的DEMO,实在感谢啊!这才是我所要的好吗!(链接在本文最后)</p>
<h2>JSONP跨域小实践</h2>
<p>你再怎么懂原理,再怎么会JSONP基本语法,不真正执行一下你其实还是懵逼。<br>当然,我认为我还是需要模拟不同源的跨域请求才对真正弄懂跨域有一定好处。</p>
<p>下面展示的代码源于那篇文章,需求是输入歌名,点击搜索后,跨域请求API接口,返回数据后,显示专辑名在页面。</p>
<pre><code><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP</title>
</head>
<body>
<input type="text" id="song" name="">
<input type="button" id="song_search" value="歌曲搜索" name="">
<br />
<div style="width:200px;height:230px;background:pink" id="song_list"></div>
<script type="text/javascript" src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script type="text/javascript">
var searchJsonCallback=function(data){
//遍历查询结果
var alb_html='';
for(var i in data.list){
alb_html+='<span>专辑:</span><div style="color:black">'+data.list[0].albumname+'</div>';
}
$("#song_list").html(alb_html);
};
$("#song_search").on("click",function(){
var keyword=$("#song").val();
if(keyword==undefined||keyword==""){
alert("歌曲搜索不能为空");
return false;
}else{
var url = "http://cgi.music.soso.com/fcgi-bin/fcg_search_xmldata.q?source=10&w="+keyword+"&perpage=1&ie=utf-8";
// 创建script标签,设置其属性
var script = document.createElement('script');
script.setAttribute('src', url);
// 把script标签加入head,此时调用开始
document.getElementsByTagName('head')[0].appendChild(script);
}
});
</script>
</body>
</html>
</code></pre>
<p>下面是我对以上代码的一些文字解释:</p>
<p>1)点击按钮后动态插入跨域数据,然后由于此接口用searchJsonCallback({})来封装json格式数据,因此可看作是调用一个函数,同时把json数据作为参数传入</p>
<p>2)所以当动态插入script标签后,写好了的searchJsonCallback()函数将被调用,参数data就是json数据,然后通过遍历渲染到DOM上,完成整个跨域获取数据流程</p>
<p>总结步骤:触发click事件-动态插入带有API接口的script标签-根据回调函数名调用函数-遍历数据-渲染到页面</p>
<h2>当JSONP遇上jQuery ajax</h2>
<p>注意了,千万不要认为用了jQuery ajax,就是通过它来实现跨域请求,其实只是因为它很好地封装了JSONP而已</p>
<p>用jQuery ajax原理和上面的是一样的,只不过我们不需要手动的插入script标签以及定义回调函数。jquery会自动生成一个全局函数来替换callback=?中的问号,之后获取到数据后又会自动销毁,实际上就是起一个临时代理函数的作用。</p>
<p>于是对上面的代码进行修改,并且把请求数据改为每次显示5条:</p>
<pre><code>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP2</title>
</head>
<body>
<input type="text" id="song" name="">
<input type="button" id="song_search" value="歌曲搜索" name="">
<br />
<div style="width:200px;height:230px;background:pink" id="song_list"></div>
<script type="text/javascript" src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script type="text/javascript">
$("#song_search").on("click",function(){
var keyword=$("#song").val();
if(keyword==undefined||keyword==""){
alert("歌曲搜索不能为空");
return false;
}else{
$.ajax({
url: 'http://cgi.music.soso.com/fcgi-bin/fcg_search_xmldata.q?source=10&w='+keyword+'&perpage=5&ie=utf-8',
type: 'GET',
dataType: 'jsonp',
jsonp:'callback',
jsonpCallback: 'searchJsonCallback',
success:function(data){
//遍历查询结果
var alb_html=''; //创建一个变量用于DOM拼接
for(var i in data.list){
alb_html+='<p style="color:black">'+data.list[i].albumname+'</p>';
console.log(data.list[i].singername);
}
var name_html='<span>专辑:</span>';
var sum_html=name_html+alb_html;
$("#song_list").html(sum_html);
}
});
}
});
</script>
</body>
</html>
</code></pre>
<p>关于jsonp、jsonpCallback两个属性,部分解释一直没看懂,如下:</p>
<p>jsonp: "callback",<br>//传递给请求处理程序或页面的,用以获得jsonp回调函数名的参数名(一般默认为:callback)<br>jsonpCallback:"handler",<br>//自定义的(?)jsonp回调函数名称,默认为jQuery自动生成的随机函数名,也可以写"?",jQuery会自动为你处理数据</p>
<p>直到有次看了下Network,懂了。其实就是拼接到url时想要显示的内容。</p>
<p>如上面情况,url则会变成"http://...&perpage=5&ie=utf-8&callback=searchJsonCallback&..."</p>
<p>所以简单来说,<br>jsonp就是相当于一个名字,一个参数名<br>jsonpCallback就是回调函数名,用来包裹JSON数据的<br>然后将jsonpCallback定义的函数赋值给jsonp定义的参数上,<br>最后拼接在url末尾完成参数传递。</p>
<p>注意:jsonp可以随便写,不写也行,反正默认是callback</p>
<p>但jsonpCallback必须是对应其返回数据的函数名,此API接口返回的数据外围用searchJsonCallback()包裹数据,因此只能是这个<br>(有些文章说这个也是可以自定义的,但我测试时随便写就报错了。。可能因为这个API接口固定了函数名吧)</p>
<p>其实上面这些例子更方便将理论用于实践,毕竟数据等都是现成的,跨域问题也是常见的开发问题,感觉就没必要大费周章才能进行模拟跨域(所以我上面说自己弄服务器那种方法略麻烦)</p>
<p>参考资料:<br>上面提到的原文链接:(可惜没有CORS举例)<br><a href="https://link.segmentfault.com/?enc=7J9Pao6cYgLOeWGxQiKXnw%3D%3D.qoBn0B7RCEzyIukk4SN68mvIEKOvHduMtm1uqsZ90NateA%2BwG0cefdVnYHPl4GKy" rel="nofollow">http://www.cnblogs.com/st-les...</a></p>
<p>另一篇帮助理解的文章:<br><a href="https://link.segmentfault.com/?enc=9Plz%2FCetGEZDaqq4unVZOQ%3D%3D.PdweHDCjvN1wAIYvqs7lWiXo5sFTOyTK%2FCkNhuzoCnY89GfqhP2J8K074rtPTXp1cwqY8GfWyXHjmepsHYI70Q%3D%3D" rel="nofollow">http://blog.csdn.net/u0139455...</a></p>
<p>还有很多良心文章就不一一列举了,这次是首次将跨域理论落实到实践,觉得很过瘾所以就写下来了。排版不太好勿见怪。大神路过如有些其他看法可以在下面留言。</p>
关于如何使sublime text3支持Vue语法高亮显示
https://segmentfault.com/a/1190000009398996
2017-05-12T20:06:44+08:00
2017-05-12T20:06:44+08:00
gyt95
https://segmentfault.com/u/gyt95
3
<p>把Vue官方文档的基础篇看完感觉好像没学到什么,找些小项目练练,结果用sublime text3时,不支持.vue文件,写什么都是白色的,没有语法高亮显示,辣眼睛。。。既然选择sublime text3,就得习惯下载插件呀。。在这里记下步骤。。。</p>
<h3>1、插件下载地址</h3>
<p><a href="https://link.segmentfault.com/?enc=RuJPTgth%2FKd9N8SZ0perRw%3D%3D.A9FJkUwyoyBIDZToHSdY1ccy7yTzBxOH3k523UsgYP001mPkonz1e5LT92GUZyai" rel="nofollow">https://github.com/vuejs/vue-...</a></p>
<p><img src="/img/bVNBfo?w=1315&h=729" alt="clipboard.png" title="clipboard.png"></p>
<p>如果不会git就点击Download ZIP,不过下载下来还要解压。<br>如果想用git命(zhuang)令(bi)的话,直接git clone ssh地址就行,克隆下来就已经是文件夹形式了。</p>
<h3>2、进入sublime text3</h3>
<p><img src="/img/bVNBgc?w=361&h=247" alt="clipboard.png" title="clipboard.png"></p>
<p>菜单项->Preferences->Browse Packages...<br>自动打开一个文件夹,其中包含了这个编译器所有插件。<br>新建一个文件夹,假设名为Vue,将刚才下载下来的文件夹中所有文件拷贝进来。<br>打开命令面板ctrl+shift+p,输入vue,选择“Set Syntax:Vue Component”进行加载。<br>一般加载都很快的,我的情况是当前打开的.vue文件立刻语法高亮显示,如果发现没反应,就重启一下sublime text3吧。</p>
关于this的知识归纳(通俗易懂版)
https://segmentfault.com/a/1190000009393621
2017-05-12T14:42:54+08:00
2017-05-12T14:42:54+08:00
gyt95
https://segmentfault.com/u/gyt95
2
<p>对this的理解,我一直都是用一句话概括:谁调用它,它就指向谁。</p>
<p>好像也没有什么问题,但仔细看了<你不知道的JavaScript>这本书和网上一些文章后,发现this的原理还挺讲究的。于是决定写个归纳。<br>(对了,无知的我实在没想到原来bind也和this扯上关系。。)</p>
<p>this的绑定规则有4种。分别是:<br><strong>1、默认绑定</strong><br><strong>2、隐式绑定</strong><br><strong>3、显示绑定</strong><br><strong>4、new绑定</strong></p>
<p>需要明确:this的值虽然会随着函数使用场合的不同而发生变化,但有一个原则,它指向的是调用它所在的函数的那个对象。</p>
<h3>1、默认绑定(纯函数调用)</h3>
<pre><code>function test(){
console.log(this.a);
}
var a = 1;
test();</code></pre>
<p>当调用test()时,因为应用了this的默认绑定,this.a被解析成全局变量a,this指向全局对象window。所以结果为1。</p>
<p>怎么知道应用了默认绑定呢?当前test()是直接使用不带任何修饰的函数引用进行调用的。这个简单来说就是没有任何前缀啊等东西,很纯粹!而其调用位置是全局作用域,更能确定除了默认绑定,无法应用其他规则了。</p>
<p>如果懂了,那么下面的例子也就会做了</p>
<pre><code>function test(){
this.a = 2;
console.log(a);
}
test();
</code></pre>
<p>已知调用函数test()的对象是window,所以this指向window,即this.a===window.a。由于window可以省略,因此简写成a。<br>相当于在全局作用域声明了变量a,并且赋值a=2。<br>this.a = 2<br>-->window.a = 2<br>-->a = 2<br>所以结果为2。</p>
<p>可能说得太啰嗦了,直接说下一种绑定。</p>
<h3>2、隐式绑定(作为方法调用)</h3>
<p>通俗地说就是一个函数,被当作引用属性添加到了一个对象中了,然后以 “对象名.函数名()” 形式进行调用,这时如果函数引用有上下文对象,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。</p>
<p>下面看下例子。</p>
<pre><code>function test(){
console.log(this.a);
}
var obj = {
a:3,
test:test
};
obj.test();</code></pre>
<p>对象obj中包含两个属性a和test,其值分别是3和一个函数test()。<br>obj.test()表示函数引用时有上下文对象(也就是这里的obj)。<br>根据隐式绑定规则,会把test()中的this绑定到这个上下文对象,即this被绑定到obj,this.a===obj.a。<br>所以结果为3。</p>
<p>如果懂了,那么下面的例子也就会做了</p>
<pre><code> function test(){
console.log(this.a);
}
var obj2 = {
a:4,
test:test
};
var obj1 = {
a:400,
obj2:obj2
}
obj1.obj2.test()</code></pre>
<p>看起来复杂了些,不过只要记住下面这个准则就可以了。</p>
<blockquote>对象属性引用链中,只有上一层或者说最后一层在调用位置中起作用</blockquote>
<p>所以只有obj2这个上下文对象生效,结果为4。</p>
<h4>隐式绑定的番外篇——隐式丢失</h4>
<p>这个网上貌似很少提及,隐式丢失,通俗来讲就是“变low”了!<br>原本应用隐式绑定的,因为丢失绑定对象,变回应用默认绑定了!把this绑定到全局对象或undefined。</p>
<p>当对象将其引用属性给了新的引用,再次调用这个新的引用时,原本this指向的对象就会改为指向window。</p>
<p>到底在说什么,看个例子。</p>
<pre><code> function foo(){
console.log(this.a);
}
var obj = {
a:5,
foo:foo
};
var bar = obj.foo; //这句就是关键
var a = "oops, global";
bar(); // "oops, global"</code></pre>
<p>b引用了test()函数本身,此时的b()是一个不带任何修饰的函数调用,因此应用了默认绑定。</p>
<p>如果懂了,那么下面的例子也就会做了</p>
<pre><code> function foo(){
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj = {
a:5,
foo:foo
}
var a = "oops, global";
doFoo(obj.foo); // "oops, global"</code></pre>
<p>其实和上面的没什么区别,只是这里把函数作为参数传递了,参数传递是一种隐式赋值。此时的this指向的是调用它的函数的对象即全局对象,因此应用了默认绑定。</p>
<h3>3、显示绑定(apply/call调用)</h3>
<p>回顾一下隐式绑定,其关键是把函数当作引用属性添加到了对象中,通过这个属性间接引用这个函数,把this简介(隐式)绑定到这个对象上。<br>如果不想在对象内部包含函数引用,而想简单粗暴地在某个对象强制调用一个函数,这时就用到了函数的call()和apply()方法。<br>call和apply,以往我单纯理解为“控制this的指向”,现在才发现是原来是this绑定规则中的一种。</p>
<p>call和apply区别<br>apply接收的是数组参数,call接收的是连续参数。所以当传入的参数数目不确定时,多使用apply。<br>(tips:这里推荐个方法记忆apply和call各自接收的参数:apply为a开头,数组Array也是a开头,所以apply接收的是数组参数)</p>
<p>看下面例子。</p>
<pre><code>function test(){
console.log(this.a);
}
var obj = {
a:6
};
test.call(obj);</code></pre>
<p>当调用test时强制把它的this绑定到obj上。所以this.a===obj.a,结果为6。 <br>注意:后续参数传入的是原始值的话,会被转换成它的对象形式(如字符串类型-->new String()如此类推)</p>
<p>这次我们不用call,用apply。</p>
<pre><code>var a = 0;
function test(){
console.log(this.a);
}
var obj = {};
obj.a = 7;
obj.m = test;
obj.m.apply();
obj.m.apply(obj);</code></pre>
<p>同样的,对象obj包含了两个属性a和m,m引用了函数test()。<br>obj.m.apply(obj)即把test()这个函数中的this绑定到对象obj上。即this指向的是obj。所以this.a===obj.a。<br>“apply没有参数怎么办,基本语法都是包含参数的啊,至少给个对象,让this有个指向啊。”<br>apply定义了当没有参数时,全局对象会自动默认成为其第一个参数,apply()等价于apply(window)。<br>因此obj.m.apply(),test()中的this就指向了全局对象window,结果为0。</p>
<h4>显示绑定的番外篇——硬绑定</h4>
<p>干嘛用的?解决前面提到的隐式丢失问题。<br>回顾当初隐式丢失的第二个例子。</p>
<pre><code> function foo(){
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj = {
a:5,
foo:foo
}
var a = "oops, global";
doFoo(obj.foo); // "oops, global"</code></pre>
<p>将其改一下变成:</p>
<pre><code> function foo(){
console.log(this.a);
}
function doFoo(fn){
fn.call(obj);
}
var obj = {
a:5,
foo:foo
}
var a = "oops, global";
doFoo(obj.foo);</code></pre>
<p>依旧是创建了doFoo()这个函数,但在其内部手动调用了 obj.foo.call(obj),<br>把foo()强制绑定到了obj对象,之后无论如何调用doFoo(),它总会手动在obj上调用foo。</p>
<p>对于硬绑定,ES5提供了一个内置方法Function.prototype.bind,我们把上面的例子再改!</p>
<pre><code> function foo(){
console.log(this.a);
}
function doFoo(fn){
fn.bind(obj);
}
var obj = {
a:5,
foo:foo
}
var a = "oops, global";
doFoo(obj.foo);</code></pre>
<p>一执行,我的天!啥也没有啊??<br>这就对了,这是bind的“效果”,也是和apply、call的主要区别——延迟调用。<br>也就是说bind其实只是函数的引用,要想执行需要进行回调,即fn.bing(obj)(),<br>此时就能输出5了。</p>
<p>好,现在把隐式丢失的第一个例子改动</p>
<pre><code> function foo(){
console.log(this.a);
}
var obj = {
a:5,
foo:foo
};
var bar = obj.foo.bind(obj);
var a = "oops, global";
bar(); //5</code></pre>
<p>为了能证明bind的特点,函数在回调时执行,把bind改成call,最后3行代码变成</p>
<pre><code>var bar = obj.foo.call(obj);
var a = "oops, global";
bar(); //Uncaught TypeError: bar is not a function</code></pre>
<p>结果报错了,对,因为call和apply都是绑定后立刻执行的,都执行完了,bar就只是一个没有赋值的变量而已。 </p>
<p>总结下“显示绑定三人组”:</p>
<pre><code> 共同点:
1、都用于控制this指向;
2、第一个参数都是this需要指向的对象,也就是上下文;
3、都可以后续参数传递;
4、没有任何参数时,this都指向全局对象window
区别:
1、call、apply绑定后立刻执行,bind是延迟执行。换言之,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,就使用bind()方法吧。
</code></pre>
<h3>4、new绑定(作为构造函数调用)</h3>
<p>什么是构造函数,一个函数被new调用时,该函数就是构造函数。<br>(变身前:普通函数;使用地摊货new变身器,变身后:构造函数)</p>
<pre><code>function test(){
this.x = 8;
}
var obj = new test();
console.log(obj.x);</code></pre>
<p>你可以简单粗暴理解为:就相当于test()被对象obj调用,其this指向obj。<br>但貌似很不规范。。</p>
<p>实际上,调用函数的是new关键字。</p>
<p>使用new来调用函数,即函数的“构造调用”时,我们会构造一个新对象,<br>并把它绑定到test()调用中的this上。所以在代码中,this就指向了新对象obj</p>
<h3>最后总结</h3>
<p>判断this的4种规则根据优先级从高到低排序如下:</p>
<p>1、函数在 new 中调用,this绑定的是这个新对象<br>2、函数通过 call、apply或bind 调用,this绑定的是指定对象<br>3、函数在某上下文对象中调用,this绑定的是这个上下文对象<br>4、以上都不是,使用默认绑定。(在全局作用域调用,this绑定window对象) </p>
<p>关于this书中还提及到很多其他知识,例如软绑定、this词法箭头函数等,就不归纳到这里了</p>
<p>归纳前看过的相关书籍或文章:<br>1、<你不知道的JavaScript>(上)第二部分第二章 this全面解析<br>2、阮一峰的网络日志-JavaScript的this用法<br><a href="https://link.segmentfault.com/?enc=zTkSKScvg%2BuSkKq6wbTQYQ%3D%3D.u8R22TJ%2FaMmWYhEZi8W%2F2egPYDBlg%2Bg%2FX%2Fb6GU0K1%2BzJcg3ku1SEe%2FZ%2B1BNzNSiang5q94AgTiRmARE7NDNpBeVFMzI1LHrO7%2Fu2FUlljTw%3D" rel="nofollow">http://www.ruanyifeng.com/blo...</a> <br>3、chanzen的个人博客-call,apply,bind用法和意义<br><a href="https://link.segmentfault.com/?enc=7WSYhGoTgKLc4cvkS4sx0Q%3D%3D.AE1QlKoFrM5wqoEK4hGm6KH3Ll%2FIDTY2AUny4X0V4ij%2BDC0Go2UuatUssYvYlBQZgSELudsCQExj1FL%2Bcl232A%3D%3D" rel="nofollow">http://ovenzeze.coding.me/use...</a> <br>4、脚本之家-JS中的this变量的使用介绍<br><a href="https://link.segmentfault.com/?enc=KGGfv9CVmEzXl6OgenbAlQ%3D%3D.Zg4FZ%2BiONMZ7%2FEuWe8L%2Bhamv0Z%2FJWB4KuZT43NGx69ZtLrBeOxLHVHff7dNe5Spl" rel="nofollow">http://www.jb51.net/article/4...</a></p>
前端开发学习-网址记录
https://segmentfault.com/a/1190000007409338
2016-11-07T19:21:21+08:00
2016-11-07T19:21:21+08:00
gyt95
https://segmentfault.com/u/gyt95
3
<p>最近在复习JavaScript知识时遇到以前就不懂的闭包、上下文,虽然比以前理解深了一点,但还是懵,想缓一下。。就去看了其他。。把Git、Grunt、Gulp、jQuery、jQuery UI、React给入门了(React的入门略困难。。)</p>
<p>原本想记录一下Grunt/Gulp的安装和命令配置的,发现SF及网上有太多文章了,以后有空简短写一下就可以了。</p>
<p>然后最近又入坑FreeCodeCamp。。原来慕课网的编程练习就是模仿他的啊。。</p>
<p>本篇文章主题是记录网址,因为自己太贪,一天下来打开了很多觉得有用的网页,但又没时间看,如下图</p>
<p><img src="/img/bVFfAB?w=356&h=589" alt="图片描述" title="图片描述"></p>
<p>所以决定把一些记事本里记录的网址发上来。。。不然实在是眼花缭乱</p>
<h2><strong>在线测试代码</strong></h2>
<p>老牌:<a href="https://jsfiddle.net/">https://jsfiddle.net/</a></p>
<blockquote><p>在线JS代码调试工具是一个老牌的支持javascript、css、html代码可视化在线调试工具,支持多种应用多种主流框架,用起来非常方便,而且还可以将调试好的结果以非常简洁的页面直接嵌入在其他网页里。<br>对于网页设计师来说需要写演示用的JavaScript实例代码的时候,就完全可以在jsFiddle里面直接完成编写后调试,再将结果直接嵌入Blog正文里即可了,真的是很方便的选择。<br>除了可以调试代码外,还可以方便的发布到社区,论坛或者社交媒体上与朋友们分享或者提问。整合了很多的不同的类库供大家选择。<br>类似的工具还有jsbin.com,也非常不错。</p></blockquote>
<p>新秀:<a href="http://codepen.io/">http://codepen.io/</a></p>
<blockquote><p>CodePen.io网站前端设计开发平台是一个针对网站前端代码设计的开发工具,提供多种效果的网站前端代码设计工具,丰富的案例特效,用户可以demo的基础上开发自己的前端设计。</p></blockquote>
<p>Codepen教学:<a href="https://link.segmentfault.com/?enc=XDSWHApTJ%2BKaUTcEfLTbJg%3D%3D.ph%2BSXDPixxQ5emiZNXehkIpq1G07aOvKJ17NN%2BviTqtyPC5FWWXZBr7xv%2BOaHtq6ugvrvDDvw4mDJiMwU53Hmg%3D%3D" rel="nofollow">http://www.zhangxinxu.com/wor...</a></p>
<h2><strong>代码格式化</strong></h2>
<p>其实用sublime text3插件也可以的,HTML-CSS-JS Prettify</p>
<p>在线代码格式化:<a href="https://link.segmentfault.com/?enc=Igmh0gy%2FCLisCWR3%2BCvU6w%3D%3D.kaGrettCXb6Ac4KI0SyXyCs%2BgxGMg5rri9mEDdGFuO5tmKKZ%2BctBqVVTBghv71eV" rel="nofollow">http://tool.oschina.net/codef...</a></p>
<h2><strong>特效库</strong></h2>
<p>CSS3+Html5在线测试以及实用教程酷站分享:<a href="https://link.segmentfault.com/?enc=a3jXql1kZSUS1v5v%2B4nQSg%3D%3D.LwjqdgS%2FulHrOw2wENolixMJeeMG2gfMtaWxQdulBBAFWOEUamLz792snNPJMcUS" rel="nofollow">http://www.25xt.com/html5css3...</a></p>
<h2><strong>前端知识体系</strong></h2>
<p>两个都不同,都很庞大。。不是一下子能看完<br>1、<a href="https://segmentfault.com/a/1190000004070468">https://segmentfault.com/a/11...</a></p>
<p>2、<a href="https://link.segmentfault.com/?enc=VHwrJjc2ijiopZJwN57MkA%3D%3D.CMC6ANFC%2BWgbVjIguHQVeuVx%2F%2BpO01aGJj7ZEdZc4%2FMRUiHNFaI2hXWYAfKCBp5BKIUeUIGAlRej%2FNdI1e%2F5lQ%3D%3D" rel="nofollow">http://blog.csdn.net/qq_34348...</a></p>
<h2><strong>综合</strong></h2>
<p>使用 freeCodeCamp 编程 是一番怎样的体验?<a href="https://link.segmentfault.com/?enc=E%2FeXXD8AdBJyhMQCPOrxWQ%3D%3D.LEESrO4XpodqW%2FDbWu8aLWkey83Q2HNFfv4%2BCkwFivB13ZM2hUT89dygHUV3bLReC3rPE51D8Uz0qfoCcKadnQ%3D%3D" rel="nofollow">https://www.zhihu.com/questio...</a><br>学习笔记:freeCodeCamp网站前端开发基础算法题<a href="https://link.segmentfault.com/?enc=1DvtTIXmQGiKhuhTx7wY0w%3D%3D.x3a9uq0KA4ZfDW%2BRy4HSZqOPK8WUwVzcjkMnXD0yLK1vB4BP2mMPzEmZE2vy9aGQ" rel="nofollow">http://ce.sysu.edu.cn/hope/It...</a><br>如何优雅地使用 Stack Overflow?<a href="https://link.segmentfault.com/?enc=Pnuv5m%2FAAYayRjgGbmfv2Q%3D%3D.cnhmy6p4mAYCDrssHX%2Bd%2FdBJ1wx4nC2pcVsH%2Bc%2FypdymvIxRogqorFCqqR5%2BN1vi" rel="nofollow">https://www.zhihu.com/questio...</a><br>如何优雅地使用Sublime Text?<a href="https://link.segmentfault.com/?enc=qz4W0td0T6xonbe9A8uxPg%3D%3D.DjlMGvGxZp0DO54E9uX3IBwu4464%2FrsPVCZqo5lW52Z7tBWhJiv9538xcu0UV2bt" rel="nofollow">http://www.html5cn.org/articl...</a><br>零度博客:<a href="https://link.segmentfault.com/?enc=7WVFOUVfj7sf8CXAPBJHZA%3D%3D.q62aWfwPON4pe1PN3o%2FuPP9TnaLHciCOghObzkJhKzA%3D" rel="nofollow">http://www.lingdublog.cc/cate/1</a><br>码农网:<a href="https://link.segmentfault.com/?enc=HmybHS23xP%2F%2FincxnigOBw%3D%3D.WmZ8HLWdv60Qqa8TuCokWJ4g5j48L0XC%2FDjCwGmUlX8%3D" rel="nofollow">http://www.codeceo.com/</a><br>伯乐在线:<a href="https://link.segmentfault.com/?enc=PU5WATJvZgdZCHRc%2Bq2eGg%3D%3D.ExsnHgZL8lwrm8FVojZPjVSjdzXumy1CVCfT6DRt7oo%3D" rel="nofollow">http://web.jobbole.com/</a><br>什么是页面渲染:<a href="https://link.segmentfault.com/?enc=LvmPbv1Y%2BSahWSY8KJcmaA%3D%3D.a3lAhAwOUvnTAAi4iBTduui6nyUycxqX1winZrbnf0oGWWtvma71QGUZ83aYxudX" rel="nofollow">http://www.zhihu.com/question...</a></p>
<p>国外:<br><a href="https://link.segmentfault.com/?enc=wa%2FzDfZph1lf2SWLcWDLrw%3D%3D.ZnL0juICzn2xYe2SwlGm%2FNotCUHQkh%2BJnW5HMcRPwL1wspFEYlCcdAarefzKTFxd2Gf58aJ0Y29RydB4AMQoOw%3D%3D" rel="nofollow">http://career.guru99.com/top-...</a><br><a href="https://link.segmentfault.com/?enc=GxHiZ2%2B8x5YXO3celpRVKg%3D%3D.XLOjhb%2FmGWtFRAM8NHPIYvBDG9ZRaNw%2FvMKUYkqMCgfrOK39krwRGIFiMlrCJ%2FPYbJFjr8M%2BMznUw30sDJtmIg%3D%3D" rel="nofollow">https://www.toptal.com/javasc...</a></p>
<p>先更到这,还有太多网址,先归类一下再补充。澄清一下,不是推荐网址!只是我没来得及看得网址汇总而已</p>
<h3>-------------2016.11.9更新------------------</h3>
<p>补充几个感觉挺全面的地址<br>css,js的学习分享:<a href="https://link.segmentfault.com/?enc=OxOPlCk014CUgkevT6yozQ%3D%3D.QPi80w%2F5LDSGBJLm3%2BU%2FtYXxRMtYhE%2B0XDfd4ahrNqNib%2Bo4Osu5Ct25u5k%2BbsFP" rel="nofollow">http://www.cnblogs.com/jikey/...</a><br>(特点:同一个知识点把相关的文章都归类了,例如闭包这一个知识点就有10篇,可能你会觉得,这么多没必要啊。。基本大同小异,但像我这种接受知识相对慢的人来说,多一篇文章,就是多一个让我更快理解知识点本质的机会)</p>
<p>原生js制作的各种实例:<a href="https://link.segmentfault.com/?enc=DivtklKqq04GQ7K6YcTXKQ%3D%3D.E%2Fm4ZZxc%2F1y3oOaCBV0Y5uAdkuC5lU4x6XbUtca%2B4BM%3D" rel="nofollow">http://www.fgm.cc/learn/</a><br>(特点:发现网上其实有很多这种的了,但是觉得这个对新手或者对JS理解不够的可以用来练手,有少量中文注释,个人感觉还不错,然后当时复制时把作者原话(?)也复制了,所以顺便贴在下面吧)</p>
<blockquote><p>写一堆作品,比如以下这样,然后写的当中,你会遇到问题,这时候不要着急,遇到问题,解决问题,就是成长的过程。然后享受这个过程,这些解决问题的经验,将来是你面试的资本,是工作中赖以生存的根本。也是促使你向更高技术攀登的动力,正因为css,js如此好玩,所以我们一直不能停止追逐他的步伐。然后我个人感觉,css,js是学不完的,技术随着需求的不断变化而不断迭代,最主要还是基础打好,以不变以万变。</p></blockquote>
<p>Web前端开发规范文档你需要知道的事:<a href="https://link.segmentfault.com/?enc=mh11WzvO5nyoaTztfroUKQ%3D%3D.5hPwP9Vsdi8k%2FBr2cdSERv4J%2Fi9xVUf6Zth3E2Z55jxIwiLXhV8mE%2FMk%2BUuNhS3fA7a3ZjFcBylzLuZtHtvZrPIjbsuwaaVCcNX9fEro5Tw%3D" rel="nofollow">http://itindex.net/detail/396...</a><br>Web前端:11个让你代码整洁的原则:<a href="https://link.segmentfault.com/?enc=K57BOzZCyuU8oJmxQ2JLbw%3D%3D.bQ5HzKQ4riWpB9bCKoO9bDoRToHO4upEB2MlZ6EME5bgkXg0MmXznYuiwvsQyx6ZWPbgK5Tz7v1i5GnLUxAJ8w%3D%3D" rel="nofollow">http://itindex.net/detail/386...</a></p>
<h3>-------------2016.11.26更新------------------</h3>
<p>由于上上周要考证,上周又病了好几天,没时间更新,FCC和github都没空弄。。唉<br>明天要旅游,这次要更多些~</p>
<h2><strong>前端调试</strong></h2>
<p>Fiddler介绍:<a href="https://link.segmentfault.com/?enc=lVWfkQ6K1XApRKaesNhDDg%3D%3D.95qBJ14vG4JmNWGYz0qS3eYaC%2Fs%2Bjh%2BLxMRKz6kwt4FeGQQ8iUvzJWd1AGxerU9mjoGqwcsl8f4LHejXzXcEP7gULLYjLc0SPHN5jfqx8su54edwCQu1hxqhROZcV8XHpEafbXBeqt%2FhHTOAQvE9SQ%3D%3D" rel="nofollow">http://www.aliued.cn/2010/04/...</a><br>Fiddler实例:<a href="https://link.segmentfault.com/?enc=yF39Fr1QlOmyqgn%2BV9IqYQ%3D%3D.tugMaLB0oelfYBy6MSfi7bGT3sWcvGRf3e5Ulutcgi9oabtyivvIX3UEPHRSL%2BUfK%2FIjeWSr9l8l8MY1Qg1kw6a7aKbUKxd0PuynptIFjCQpQdfEHk6S98AigUtW%2Bn3D7U3meJW5q7fTuDxyBKfWKg%3D%3D" rel="nofollow">http://www.aliued.cn/2010/04/...</a><br>(就是用于前端调试。。曾在学习Ajax时用来测试客户端向服务器请求数据的具体情况,简化版:<a href="https://link.segmentfault.com/?enc=se7Rw%2FVMA0bXjkn3vvbomw%3D%3D.YkzoHVFL3EP0hVCKibqYlPoOqbfGtIvUmuDhH4M5p7YTENP0yqtkZZfzxASUGZVT3LkipS%2Fdx5LesolntfQyKA%3D%3D" rel="nofollow">http://www.cnblogs.com/shijin...</a>)</p>
<h2><strong>一些博客</strong></h2>
<p>时间定格1:<a href="https://link.segmentfault.com/?enc=1rdp%2FkO9rrcGJHipBDAqSg%3D%3D.QWflqhSumXMthxivlCKCdW5KOAy2UjSad5qhKfGIJ2fkHf2SyCzlVmkcYHr0FIeaDP8VWfItHFwRmu6dC4f5kw%3D%3D" rel="nofollow">http://www.cnblogs.com/NetSos...</a><br>时间定格2:<a href="https://link.segmentfault.com/?enc=aLbv6LD8sDdO7eHyb2049A%3D%3D.i86pU8AWXsTAna2ihfPZfB8PHD9JxNYscGQ4n6ifgwo%3D" rel="nofollow">http://www.html5jscss.com/</a><br>(特点:文章涉及的知识很广很杂乱。。某些技术总结不错的,但只适合像杂志一样随便看下就好)<br>(缺点:很旧很旧,前者大部分文章已经是2010年,后者大多数停在2014年)</p>
<p>月光博客:<a href="https://link.segmentfault.com/?enc=mrJtosGl5mveyeW56YA05g%3D%3D.GItP678Cb%2BfhOCcrZxGpbMuDs3qaySUr4jkeO9frMys%3D" rel="nofollow">http://www.williamlong.info/</a> <br>(如果觉得前面那个博客有点过时,最多只适合新手浏览一下,那这个博客你应该不会这么说了,要是真的很热爱互联网方面知识,月光博客真的很不错,也是当杂志看,不过资讯很新鲜,基本每天更一篇)</p>
<h2><strong>良心资源库</strong></h2>
<p>知识库:<a href="https://link.segmentfault.com/?enc=5t%2B7SJoHrqtg67tE7N%2B4gw%3D%3D.zvTgLCO9EU13hm8L87H5%2BgdS3wapglwaNxLDutdHBiA%3D" rel="nofollow">http://lib.csdn.net/</a>(这个涉及面很广,内容很多,有空多逛逛)<br>伯乐在线:<a href="https://link.segmentfault.com/?enc=4eASqdmogw02SJg4eaYeyA%3D%3D.VJ6JD1F7H0ZQxANWLMv1jCJJefhJ7XnLJi9OZ8aeNac%3D" rel="nofollow">http://hao.jobbole.com/</a>(这个也是内容很多=.=有空多逛逛)</p>
<h3>-------------2017.10.13更新------------------</h3>
<p><a href="https://link.segmentfault.com/?enc=AjQoqxeQULJoK%2FqTwPIppg%3D%3D.auj9WP1wpqYOqUkGFGoq72EuxQ6QT4axNCkH7c1bi8I%3D" rel="nofollow">https://devdocs.io/</a><br>无意中找到的一个挺好的开发文档集合的网站,可以在线查询多种编程语言及框架的API。感觉非常好用,不过我这个是英文版,要中文版的童鞋自己私下找找吧,好像可以选择离线查看的。</p>
<p><a href="https://link.segmentfault.com/?enc=b0fmGFNRMj5Ga0GCKhE8SA%3D%3D.UqDiQ%2FtpUhPNg4KF5QFtJ6HqiRDsGlh11ykn8bTPaSiu4JlN02HrgSgQ1qRQuEWF" rel="nofollow">https://developer.mozilla.org...</a><br>在freecodecamp刷题时官方提供的语法地址来自这里,第一次知道这个网站,感觉挺好,我个人觉得是升了几级的w3school哈哈哈</p>
<pre><code>
————WEB前端学习,代码读百遍其义必见。</code></pre>
JS内置对象-Array数组对象的一些常用方法区分
https://segmentfault.com/a/1190000007346113
2016-11-01T16:32:12+08:00
2016-11-01T16:32:12+08:00
gyt95
https://segmentfault.com/u/gyt95
0
<p>第一篇篇幅太长了,自己回顾都觉得有点伤神。。以后尽量多篇少字~</p>
<p>首先简单介绍Array数组对象</p>
<h2>什么是数组:</h2>
<blockquote><p>用单独的变量名存储一系列的值</p></blockquote>
<h2>如何创建数组:(有3种方法)</h2>
<p>1、常规方式:</p>
<pre><code>var gyt=new Array();
gyt[0]="aaa";
gyt[1]="bbb";
gyt[2]="ccc";</code></pre>
<p>2、简洁方式:</p>
<pre><code>var gyt=new Array("aaa","bbb","ccc");</code></pre>
<p>3、常用方式:</p>
<pre><code>var gyt=["aaa","bbb","ccc"];</code></pre>
<h2>如何进行数组访问:</h2>
<p>通过指定数组名和索引号访问,以下是访问gyt数组的第一个值</p>
<pre><code>var name=gyt[0];
</code></pre>
<p>sort(a,b)排序,a-b,升序;b-a,降序<br>reverse()把你原先设置的数组中的元素排序整个调转</p>
<p>然后就是本篇重点,push()方法的用法、它的死对头unshift(),以及push()与concat()区别</p>
<h2>---------------末尾追加push()---------------</h2>
<p>首先~<br>定义:push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。 <br>语法:push(要添加到数组的第一个元素,第二个,第三个...)<br>返回值:新的长度,新的长度,新的长度</p>
<p>新的长度嘛,一看就懂啊,对,当初我也懂了,但敲码时还是被卡了一下,脑子一下子转不过来,立刻进入代码环节~</p>
<pre><code> <script>
var e=["a","b"];
document.write(e.push("k")+"<br/>");//结果显示:3
var f=["a","b"];
f.push("c");
var g= ["a","b"];
g.unshift("123","KK","GGEE");//开头追加unshift()
document.write(e+"<br/>");
document.write(f+"<br/>");
document.write(g+"<br/>");
document.write(g.push()+"<br/>");
</script></code></pre>
<p>由于想结果清晰一点,每个都加“<br/>"了, 虽然知道这样不好。。但重点不是这里啊。。<br>输出的结果如下:</p>
<pre><code>3
a,b,k
a,b,c
123,KK,GGEE,a,b
5</code></pre>
<p>虽然其实一看就懂了,但还是想说一下<br>1、document.write(e.push("k")+"<br/>");是指输出e.push()的返回值,即新数组的长度<br>2、f.push("c");是指单纯的push的话只是把c这个字母添加到f中<br>3、document.write(f+"<br/>");这才是输出f这个数组,当初和1有点搞混了(现在在看,发觉以前的自己是真的蠢。。)<br>4、g.unshift("123","KK","GGEE");就是我前面提到的push()的死对头,push()是末尾追加新的元素,而unshift是和它完全相反,是在数组开头追加新的元素。</p>
<p>“不是开头就是结尾,那么死板谁会用啊,还说是常用方法”,没办法了,这个时候拓展一下知识,对,就是他,splice()。<br>此方法本人称其谓数组霸道法,能改变原始数组,想删谁就删谁,想在哪追加就在哪追加</p>
<p>下面是splice()简单介绍<br>定义:向/从数组中添加/删除项目。<br>语法:arrayObject.splice(添加/删除项目的位置,要删除的项目数量,要添加的第一个元素,第二个,第三个..)<br>返回值:被删除的项目,如果有的话</p>
<p>其中,第二个参数如果设置为 0,则不会删除任何项目。代码如下:</p>
<pre><code><script>
var arr = new Array(3)
arr[0] = "tom";
arr[1] = "ben";
arr[2] = "小明";
document.write(arr+"<br>");
arr.splice(1,0,"k");
document.write(arr + "<br/>");
</script></code></pre>
<p>结果</p>
<pre><code>tom,ben,小明
tom,k,ben,小明</code></pre>
<p>如果想删除ben这个名字的话,改为arr.splice(1,1,"k");即可,就不再贴代码了。</p>
<p>最后是push()和concat()的区别<br>concat()作用是合并多个数组,返回值是新的结果!!!简单示例:</p>
<pre><code> var a=["hello","world"];
var b=["haha","you"];
var c=a.concat(b);
document.write(c+"<br>");</code></pre>
<p>而push(),经过上面这么啰嗦的解释就知道了,它是末尾追加,返回值是新的数组的长度啊!!!</p>
<p>其实还有join(),又是和concat()很类似的,就不延伸了。毕竟都是小知识。</p>
<pre><code> ————WEB前端学习,学而不思则罔,无脑敲码等于浪费时间
</code></pre>
JS内置对象-关于String字符串对象的2个小实验
https://segmentfault.com/a/1190000007315773
2016-10-28T21:38:21+08:00
2016-10-28T21:38:21+08:00
gyt95
https://segmentfault.com/u/gyt95
0
<p>第一篇技术文章写些简单点的~<br>在大三上web前端开发课程时,虽然能用JavaScript制作一些简单的页面动态效果,但其实很多JS知识并未掌握,所以自己又通过视频再复习一次JS。(我的JS书籍还在来的路上,在此之前,让我先用“在线课程”这种快餐充饥~)</p>
<p>JS内置对象中String字符串对象有太多方法了,今天通过2个简单实验,熟悉indexOf()、lastIndexOf()、charAt()3个方法的使用。</p>
<p><strong>首先来看看w3school里是怎么介绍indexOf()的</strong></p>
<p><strong>语法:</strong></p>
<blockquote><p>Object(string|array).indexOf(searchValue, fromIndex);</p></blockquote>
<p><strong>用法:</strong></p>
<blockquote><p>返回某个指定的字符串值在字符串中首次出现的位置</p></blockquote>
<p><strong>两大参数:</strong></p>
<blockquote>
<p>参数1 searchValue 必需。规定需检索的字符串值 </p>
<p>参数2 fromIndex 可选。规定在字符串中开始检索的位置。它的合法取值是0到stringObject.length-1。如省略该参数,则将从字符串的首字符开始检索。</p>
</blockquote>
<p>然后学习时所给demo实在是太简单了吧。。-.-!!!</p>
<pre><code><script>
var str="Hello World";
document.write(str.indexOf("World"));
document.write(str.indexOf("world"));
</script>
</code></pre>
<p>第一个由于原字符串有此<em>一系列完整且连续的</em>字符,于是输出位置为6,注意空格也占一个字符位置哦~<br>第二个由于没有,所以输出-1</p>
<p><strong>这里注意了~返回值是数字(索引),在后面提及的小实验中索引index的定义非常关键。</strong></p>
<h2>---------------小实验1---------------</h2>
<p>实在接受不了如此直白的demo,于是仔细想想到底这个方法能用来做什么?<br>下面是一个我觉得很有效但自己却很少用到的深入学习法<br>1、indexOf是找到首次出现的位置。。(首先多次理解这个定义)<br>2、那如果这是一个游戏。。(从感兴趣的方向作为切入点,联想实际生活)<br>3、游戏规则是需要找出一句话中"l"字母出现的所有位置。。(将其具体化)</p>
<p><strong>而indexOf()只会找一次,那要如何设计?</strong></p>
<p>于是二话不说,几行代码就写出来了,非常的迅速!</p>
<pre><code><script>
var str="hello world";
for(var i=0;i<str.length;i++){
document.write(str.indexOf("l"));
}
</script></code></pre>
<p>刷新页面一看,oh my god!(下面是运行结果)</p>
<pre><code>22222222222</code></pre>
<p>这么简单的程序都不会写,真得好好检讨一下!T.T</p>
<p>问题出在哪里呢?首先是定义一个字符串str,然后for循环让它每找到一个"l"字母就输出该字母所在位置,但是为何每次都输出2?</p>
<p>没错,关键就是那个index索引!<br>愚蠢的我以为i++就代表起始查找位置在后移,其实真正起作用的是索引index。<br>另外,还有一点!就是indexOf()方法中的参数2,刚才我的错误代码中,indexOf()是只有参数1的,参数2默认为字符串首字符位置,即要从头开始寻找。</p>
<p><strong>又没有索引值,又每次都得从头找,还要for循环,不出现一堆2才怪呢~</strong></p>
<h2>正确做法</h2>
<pre><code><script>
var str="hello world";
var index=-1;//索引值默认为-1
for(var i=0;i<str.length;i++){
index=str.indexOf("l",index+1);//每次都从上一次索引值的下一个开始寻找,找到后更新索引值index
if(index!=-1){
document.write(index);
}
else
break;
}
</script></code></pre>
<p>结果如下:</p>
<pre><code>239</code></pre>
<p>但是这样并不好看,尝试优化。希望索引值之间用“、”隔开,而最后的索引值后面无“、”。这时需要用到indexOf()的兄弟方法lastIndexOf(),只要记住用法和indexOf()完全相反,省略参数2则从字符串最后一个字符开始寻找,直到找到指定字符的最后出现位置。</p>
<p>优化代码:</p>
<pre><code><script>
var str="hello world";
var index=-1;
var a=str.lastIndexOf("l");
for(var i=0;i<str.length;i++){
index=str.indexOf("l",index+1);
if(index!=-1){
document.write(index);
if(index!=a){ //这个判断用于防止最后一个索引值后面还有“、”
document.write("、");
}
}
else
break;
}
</script></code></pre>
<p>结果查看:</p>
<pre><code>2、3、9</code></pre>
<p>其实用break来跳出循环一直觉得不太规范。。。网上看到有用do-while()方法感觉更好。。</p>
<h4>PS:要注意indexOf()对大小写要求很高,一开始没留意,写成indexof(),一直没效果,后来才发现原来o要大写。。香菇~</h4>
<h2>---------------小实验2---------------</h2>
<p>如果将游戏显示简化一点又要怎么设计呢?现在已有的游戏规则是数有多少个"l"字母,然后说出每个的位置。<br>现在增加难度,要显示次数最多的字符。</p>
<p>小实验1是显示次数(其实就是那个索引值啦0.0),这次显示的是字符哦~涉及到字符,就要用到charAt()方法。</p>
<p><strong>同样的,首先在看看w3school有什么想说的</strong></p>
<p><strong>语法:</strong></p>
<blockquote><p>stringObject.charAt(index);</p></blockquote>
<p><strong>用法:</strong></p>
<blockquote><p>返回指定位置的字符</p></blockquote>
<p><strong>参数:</strong></p>
<blockquote><p>index 必需。表示字符串中某个位置的数字,即字符在字符串中的下标。</p></blockquote>
<p>这一次涉及到for in的用法,这里要和for循环做一个区分。</p>
<pre><code>for - 循环代码块一定的次数
for/in - 循环遍历对象的属性</code></pre>
<p>遍历对象的属性,在这次实验里,由于要找出哪个字符次数最多,所以要遍历的是字符(对象)的次数(属性)。</p>
<p>简单例子:</p>
<pre><code>var a=["aa","bb","cc"];
for(var c in a){
document.write(a[c]);
}</code></pre>
<p>结果为:</p>
<pre><code>aabbcc
</code></pre>
<p>在了解for in用法后开始编写~<br>思路如下:<br>1、首先得创建一个对象obj<br>2、for循环时用charAt()方法把每个字符塞到char中,obj[char]相当于“该字符的次数”<br>3、每次循环时更新对应字符次数<br>4、定义次数最多的字符max<br>5、for in循环遍历次数,与obj里每个字母对应的次数进行比较,输出最多次数所对应的字符</p>
<p>展示各个字符及对应的次数,代码如下:</p>
<pre><code><script>
var str="hello world";
var obj={};
for(var i=0;i<str.length;i++){
var char=str.charAt(i);//用于存放字符,char就是字符
if(obj[char]){//obj[char]即char字符的次数,若次数为1次,则可以累加,否则初始化为1
obj[char]++;
}else {
obj[char]=1;
}
document.write("<br/>"+char+":"+obj[char]+" 次");
}
</script></code></pre>
<p>输出结果:</p>
<pre><code>h:1 次
e:1 次
l:1 次
l:2 次
o:1 次
:1 次
w:1 次
o:2 次
r:1 次
l:3 次
d:1 次</code></pre>
<p>可以说实验2已经完成了一半,接下来是for in循环遍历次数,找次数最多的出来!后续代码如下:</p>
<pre><code>var max=0;//次数最多,初始化为0,防止undefined
var maxChar;
for(char in obj){
if(max<obj[char]){//如果max不是次数最多的话,就更新max
max=obj[char];
}else{//如果max是次数最多的,就把char值赋给maxChar
maxChar=char;
}
}
document.write("<br/>次数最多的字符是"+maxChar);
document.write("<br/>一共出现了"+max+" 次");</code></pre>
<p>看起来行得通,结果却不尽如人意啊T.T</p>
<pre><code>次数最多的字符是d
一共出现了3 次</code></pre>
<p>为什么次数最多的字符是d。。。但又竟然是3次??明明d只出现1次,而l才是出现了3次。。咋就混在一起了呢。。。当时的我竟然想笑。。。</p>
<p>仔细回看代码,会发现if-else语句有点奇怪,按顺序执行的话,"hello world"首先是h字母次数,次数为1,max<obj[char]成立更新max,然后程序就继续循环了!这个时候的maxChar是什么?可以说是为null,为什么又会是d呢?因为到最后一次遍历时,max已经是最多次数了,这时才执行else语句,maxChar=char,把最后的一次遍历的字符d赋给了maxChar。</p>
<p>换言之,根本不需要else语句,当更新max的同时,也要更新maxChar,对号入座,才不会有“牛头不搭马嘴”这种尴尬情况的发生。。。</p>
<p>完整代码如下:(由于我们要的只是最终结果,所以“列举每个字母及对应的次数”这一步可省略)</p>
<pre><code><script>
var str="hello world";
var obj={}
for(var i=0;i<str.length;i++){
var char=str.charAt(i);//用于存放字符,char就是字符
if(obj[char]){//obj[char]即char字符的次数,若次数为1次,则可以累加,否则初始化为1
obj[char]++;
}else {
obj[char]=1;
}
//document.write("<br/>"+char+":"+obj[char]+" 次");
}
var max=0;//次数最多,初始化为0,防止undefined
var maxChar;
for(char in obj){
if(max<obj[char]){//如果max不是次数最多的话,就更新max
max=obj[char];
maxChar=char;
}
}
document.write("<br/>次数最多的字符是"+maxChar);
document.write("<br/>一共出现了"+max+" 次");
</script>
</code></pre>
<p>第一篇文章就用了我3小时。。。只想说一句。。好累!虽然这只是JS中一个很小的知识,不过还是挺有意义的~</p>
<p>最后感谢博客园用户!master的这篇文章,<a href="https://link.segmentfault.com/?enc=WJEPQ3091gAkYd6Wu2CvCQ%3D%3D.3AyTK09FE%2BCgqpzFP4eyjyJXDdwl9Q%2FWr3TRX8SvlRHhXzNjeEiyR0Mmt5k7JSBMUsz0qUGSNrBhq3lTWs4F8g%3D%3D" rel="nofollow">js--找字符串中出现最多的字符</a>,实在没想到有人会和我想到一块去了。实验2就是受这篇文章的启发的。</p>
<pre><code> ————WEB前端学习,需要我们多看书多敲码多思考
</code></pre>