首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
69 阅读
2
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
52 阅读
3
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
42 阅读
4
如何在 Clash for Windows 上配置服务
40 阅读
5
如何在 IOS Shadowrocket 上配置服务
40 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,061
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1061
篇与
的结果
2024-10-20
如何判断一个原生方法是否被重写
浏览器根据 ECMScript 标准,为我们提供了很多原生的方法。但有的脚本会重写该方法,那么如何判断这个方法是否被重写了呢?在一些性能监控和错误监控的类库中,通常都会劫持并重写这些方法,例如XMLHttpRequest.prototype.send和fetch等方法。那么我们如何知道浏览器提供的原生方法是否被重写呢?这里有两种方法可以进行检查:toString()和name。最保险的就是toString()了。因为:如果是在内置函数或由 Function.prototype.bind 返回的函数上调用 toString(),则 toString() 返回原生代码字符串,如下:const str = 'function () { [native code] }'; 我们直接执行:fetch.toString(); XMLHttpRequest.prototype.send.toString(); 就会输出如下的结果:在一些性能监控的类库中,他们为了方便统计接口的耗时、成功率等信息,会劫持这些方法,并添加上自己的统计逻辑。我们以fetch为例:const myFecth = window.fetch; window.fetch = function (...agrs) { const startTime = performance.now(); return myFecth(...agrs).then((response) => { // 假装上报数据 console.log('reportCgi', { useTime: performance.now() - startTime, }); return response; }); }; 这样既能上报性能数据,又不影响 fetch 方法本来的使用。但重写 fetch 方法后,toString()和name属性拿到的信息就不一样了!我们现在再执行如下的代码:fetch.toString(); // function (...agrs) {} fetch.name; // "" 可以发现得到的函数体内容发生了变化,同时 name 也变成了空。那么可能就有人问,如果有人在重写 fetch 方法,把 toString 和 name 也重写了呢?fetch.toString = () => 'function () {[native code]}'; fetch.name = 'fetch'; 这样再执行 toString 方法时,得到的就是类似于原生的答案。如果他重写了 toString,那么我们就看 toString 的 toString()返回了啥,总能得到结果。
2024年10月20日
2 阅读
0 评论
0 点赞
2024-10-20
axios 源码系列之拦截器的实现
我们在使用 Axios 的过程中,或多或少地要用到它的拦截器,例如要实现: 数据转换; 添加额外的数据; 输出或上报接口的请求时间、失败率等数据; 这些需求,使用拦截器就能非常容易地实现。那么 axios 的拦截器怎么使用,内部又是怎么实现的,这篇文章让我们一探究竟。 1. 拦截器的使用 # 在 axios 中,拦截器分为请求拦截器和响应拦截器。顾名思义,请求拦截器是在发出请求之前按照顺序执行的,响应拦截器是在收到响应之后(无论接口返回的是否成功)按照顺序执行的。如果我们要统计每个接口的耗时,可以先在请求拦截器中添加一个时间戳,在响应拦截器中减去这个时间戳,就是这个请求的完整耗时:// 获取当前时间 const getTime = () => { if (typeof performance?.now === "function") { return window.performance.now(); } return Date.now(); }; // 接口上报 const reportCgi = (response, config) => { // 响应失败时response为空 const { config: conf } = response || { config }; // 在响应拦截器中计算这个请求的耗时 console.log("response", conf.url, getTime() - conf.requestime); }; axios.interceptors.request.use((config) => { // 在请求拦截器中添加发起请求的时间 return { ...config, ...{ requesttime: getTime() } }; }); axios.interceptors.response.use( (response) => { reportCgi(response); return response; }, (error) => { reportCgi(error.response, error.config); return error; } ); 同时,我们还能添加多个请求拦截器和响应拦截器:axios.interceptors.request.use((config) => { // 这里假设要先获取一个token return new Promise((resolve) => { setTimeout(() => { resolve({ ...config, ...{ token: Math.random() } }); }, 500); }); }); axios.interceptors.request.use((config) => { // 在请求拦截器中添加发起请求的时间 return { ...config, ...{ requesttime: getTime() } }; }); 除此之外,axios 的拦截器还能做很多事情,如输出请求 log 和响应 log,方便在移动端进行调试;上报接口的统计数据等。 2. 拦截器是怎么实现的 # 拦截器在我们进行接口请求时,非常的方便。那么它内部是如何实现的呢?如何维护多个拦截器并按照顺序执行的呢? 2.1 拦截器的实现 # 这里的关键文件就是 InterceptorManager.js,这里的代码也比较少,我们一点一点地看它是怎么实现的:var utils = require("./../utils"); function InterceptorManager() { // 存储所有的拦截器,但请求拦截器和响应拦截器是分开的 this.handlers = []; } /** * 添加拦截器 * fulfilled: 成功时执行的,在Promise.resolve中 * rejected: 失败时执行的,在Promise.reject中 * * 返回当前添加的拦截器的ID,用于清除这个拦截器 */ InterceptorManager.prototype.use = function use(fulfilled, rejected) { // 把传入的在resolve和reject中要执行的方法添加到数组中 this.handlers.push({ fulfilled: fulfilled, rejected: rejected, }); return this.handlers.length - 1; }; /** * 根据id请求拦截器 * * id: 刚才use方法返回的那个数据 */ InterceptorManager.prototype.eject = function eject(id) { if (this.handlers[id]) { this.handlers[id] = null; } }; /** * 迭代所有的拦截器 * * 这里会跳过之前使用eject方法设置为null的拦截器 * * @param {Function} fn 对所有拦截器都执行的一个方法 */ InterceptorManager.prototype.forEach = function forEach(fn) { utils.forEach(this.handlers, function forEachHandler(h) { if (h !== null) { fn(h); } }); }; module.exports = InterceptorManager; InterceptorManager 维护着 handlers 里面所有的拦截器,对外提供了 3 个方法: use: 添加拦截器,接受 2 个参数,一个是 Promise 成功时执行的,第 2 个时 Promise 失败时执行的; eject: 根据 id 清除这个拦截器; forEach: 循环所有的拦截器,并跳过所有为空的拦截器; InterceptorManager 并不区分是请求拦截器还是响应拦截器,它只是维护他自己的一组拦截器罢了。若创建多个对象,即可分别维护各自的拦截器。function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), // 请求拦截器 response: new InterceptorManager(), // 响应拦截器 }; } 将拦截器添加到 interceptors 的 request 和 response 两个属性中后,我们就可以像上面的那样调用 use 方法添加拦截器了。request 中维护的是请求拦截器,response 中维护的是响应拦截器。 2.2 将拦截器串联起来 # 创建一个chain数组,把所有的拦截器都放进去。我们首先把真正请求接口的方法放进去:// dispatchRequest 用于请求数据,这里我们先展示不管怎么实现的 // 这里把 dispatchRequest 也当做拦截器添加到队列中 // 每2个是一组,前面用于Promise.resolve, 后面的1个用户Promise.reject var chain = [dispatchRequest, undefined]; 然后把请求拦截器放到 chain 的前面,因为我们要在发起请求之前先执行请求拦截器:// 拦截器调用forEach方法,把每一个请求拦截器都添加到chain的前面 this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { // 每2个是一组,前面用于Promise.resolve, 后面的1个用户Promise.reject // 由此也能看到,越是后添加的请求拦截器,越会是先执行 chain.unshift(interceptor.fulfilled, interceptor.rejected); }); 再把响应拦截器方法 chain 的后面,因为我们要在收到响应之后才执行响应拦截器:// 拦截器调用forEach方法,把每一个响应拦截器都添加到chain的后面 this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { // 响应拦截器按照顺序执行 chain.push(interceptor.fulfilled, interceptor.rejected); }); 现在已经把所有的请求拦截器、数据请求和响应拦截器都串联起来了:然后依次执行就可以:// 把config初始化为一个Promise对象,方便后面的使用 var promise = Promise.resolve(config); while (chain.length) { // 依次取出执行resolve和reject方法 // 将执行后的结果传给下一个拦截器 promise = promise.then(chain.shift(), chain.shift()); } 拦截器的功能就实现啦。 3. 总结 # 我们学习了 axios 中拦截器的思路,也可以在自己实现的一些功能组件中,使用这种机制,方便更多功能的扩展。
2024年10月20日
2 阅读
0 评论
0 点赞
2024-10-20
必然会用到的 axios 中自带的工具方法
在 axios 中,使用到了很多基础的工具方法,这些方法我们也可以提炼下,看是否能应用到我们自己的实际项目中。 1. 是否是绝对链接 # 所谓的绝对链接指的是有协议的链接,例如https://, weixin://, file://等,或者只有//开头的链接,都属于绝对链接,其他的则属于相对链接。/** * Determines whether the specified URL is absolute * * @param {string} url The URL to test * @returns {boolean} True if the specified URL is absolute, otherwise false */ module.exports = function isAbsoluteURL(url) { // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); }; 2. 将 baseURL 和 requestedURL 拼接成一个 URL # 这里通常 baseURL 是比较固定的,在写配置时,可以只写 requestedURL 部分,然后再将 baseURL 和 requestedURL 拼接成一个完成的 URL。在完成这个功能之前,我们首先要实现将一个 url 合法的前后部分拼接起来的方法,这里假定所有的传入都是合法的,是因为在调用该方法前已经做好了判断:/** * Creates a new URL by combining the specified URLs * * @param {string} baseURL The base URL * @param {string} relativeURL The relative URL * @returns {string} The combined URL */ module.exports = function combineURLs(baseURL, relativeURL) { // 将baseURL最后的斜杠和relativeURL最前面的斜杠去掉 return relativeURL ? baseURL.replace(/\/+$/, "") + "/" + relativeURL.replace(/^\/+/, "") : baseURL; }; 这时,再要拼接 baseURL 和 requestedURL 时,就要判断 baseUrl 是否存在,或者 path 是否是一个绝对链接,只有在 baseURL 存在,且 requestedURL 不是绝对链接时,才将 baseURL 和 requestedURL 拼接到一起:/** * Creates a new URL by combining the baseURL with the requestedURL, * only when the requestedURL is not already an absolute URL. * If the requestURL is absolute, this function returns the requestedURL untouched. * * @param {string} baseURL The base URL * @param {string} requestedURL Absolute or relative URL to combine * @returns {string} The combined full path */ module.exports = function buildFullPath(baseURL, requestedURL) { if (baseURL && !isAbsoluteURL(requestedURL)) { return combineURLs(baseURL, requestedURL); } return requestedURL; }; 在使用 url 之前,先调用下buildFullPath方法,就可以确定这一定是一个绝对链接(要保证 baseURL 和 requestURL 至少有 1 个是绝对链接)。 3. 迭代执行数据中的每一项 # 我们有时会对一个 array 类型或者 object 类型的数据进行循环迭代,执行同一个操作,例如编码其中的每一项等。但在不知道数据类型的前提下,我们可以使用下面的这个方法:/** * Iterate over an Array or an Object invoking a function for each item. * * If `obj` is an Array callback will be called passing * the value, index, and complete array for each item. * * If 'obj' is an Object callback will be called passing * the value, key, and complete object for each property. * * @param {Object|Array} obj The object to iterate * @param {Function} fn The callback to invoke for each item */ function forEach(obj, fn) { // Don't bother if no value provided // 如果为空则直接返回 if (obj === null || typeof obj === "undefined") { return; } // Force an array if not already something iterable // 如果既不是array类型,也不是object类型,则将其放到一个数据中 // 方便进行循环处理 if (typeof obj !== "object") { /*eslint no-param-reassign:0*/ obj = [obj]; } // 这里isArray采用 Object.prototype.toString.call 进行判断 // Object.prototype.toString.call(obj) === '[object Array]' if (isArray(obj)) { // Iterate over array values // 迭代数据中的每一项 for (var i = 0, l = obj.length; i < l; i++) { fn.call(null, obj[i], i, obj); } } else { // Iterate over object keys // 迭代object类型数据中的每一项 for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { fn.call(null, obj[key], key, obj); } } } } 4. 将 object 类型的参数拼接到 url 后面 # 我们在开发过程中,object 类型的数据操作起来比较方便,但在进行 get 请求时或者需要打开带参数的 URL 时,都需要把 object 类型的数据进行转换。实现这个功能最常见的库就是qs了:import { stringify } from "qs"; const params = { name: "wenzi", score: "98", company: "tencent", }; // qs中的stringify自带编码 console.log(stringify(params)); // name=wenzi&score=98&company=tencent 在 axios 中,为了方便进行用户进行配置,这里改为了根据用户传入的数据变换方式来处理数据:function encode(val) { return encodeURIComponent(val).replace(/%3A/gi, ":").replace(/%24/g, "$").replace(/%2C/gi, ",").replace(/%20/g, "+").replace(/%5B/gi, "[").replace(/%5D/gi, "]"); } /** * Build a URL by appending params to the end * * @param {string} url 要拼接的基本url * @param {object} [params] 将要追加的参数 * @returns {string} 已拼接好的URL */ module.exports = function buildURL(url, params, paramsSerializer) { /*eslint no-param-reassign:0*/ // 若没有参数,则不需要拼接,直接返回url接口 if (!params) { return url; } var serializedParams; if (paramsSerializer) { // 若用户传入了变换数据的函数,则执行 serializedParams = paramsSerializer(params); } else if (utils.isURLSearchParams(params)) { // 若是一个URLSearchParams类型的数据,直接转为string类型 serializedParams = params.toString(); } else { var parts = []; // params是其他类型时 // 对params进行循环 utils.forEach(params, function serialize(val, key) { if (val === null || typeof val === "undefined") { return; } if (utils.isArray(val)) { // 若值是一个数据,则后端可能就是要接收数组格式的数据 // 这里把key添加一个[] key = key + "[]"; } else { // 将val设置为数组,继续循环 val = [val]; } utils.forEach(val, function parseValue(v) { if (utils.isDate(v)) { v = v.toISOString(); } else if (utils.isObject(v)) { v = JSON.stringify(v); } parts.push(encode(key) + "=" + encode(v)); }); }); // 取出的所有的参数进行拼接 serializedParams = parts.join("&"); } if (serializedParams) { // 判断url中是否存在hash路径,若存在,则只获取前面的部分 var hashmarkIndex = url.indexOf("#"); if (hashmarkIndex !== -1) { url = url.slice(0, hashmarkIndex); } // 拼接 url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams; } return url; }; 5. 总结 # 这些工具方法虽然是 axios 中的,但我们平时中用到的也比较多,这里提炼出来,也希望能给大家在平时开发中,提供一些帮助。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
axios 源码系列之如何取消请求
我们在前后端交互的过程中,通常是通过请求接口来实现的,而一个页面中的交互又非常复杂,例如需要多次频繁请求同一个接口,或者在接口还没返回时就要切换路由等。这些都需要对接口请求的时机或者请求接口之后进行处理,避免一些无用的请求或者接口返回顺序的差异。 防抖:在用户快速地交互过程中,只使用最后一次交互产生的数据,然后再发起请求,例如频繁的切换 tab,或者快速输入数据等; 锁状态:在上一个接口没有返回数据时,交互状态一直处于 loading 的锁定状态,直到数据正确返回或者超时等异常; 取消上一个请求:在发起下一个请求前,把之前的请求取消掉; 前两种方式,是在发起请求前进行控制,即控制发起请求的时机,而当请求发出之后则不再控制;而最终一种方式则是取消中断还在路上的请求,然后再发起一个新的请求,不用管发起的时机。这几种方式也要看业务的需要,选择最适合的即可。我们在之前的如何实现 axios 的自定义适配器 adapter文章里,略过了 axios 是如何主动取消当前请求的。今天我们就将一下在 axios 中如何取消之前发起的请求,源码中又是怎样实现的。 1. 主动取消之前发起的请求 # 我们先来看下 axios 中取消请求的用法:const CancelToken = axios.CancelToken; // 返回两个字段,{ token, cancel } // token用于表示某个请求,是一个Promise类型 // cancel是一个方法,当被调用时,则取消token注入的那个请求 const source = CancelToken.source(); axios .get('/user/12345', { cancelToken: source.token, // 将token注入到请求中 }) .catch(function (thrown) { // 判断是否是因主动取消导致的 if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // handle error console.error(thrown); } }); axios.post( '/user/12345', { name: 'new name', }, { cancelToken: source.token, } ); // 主动取消请求 // cancel方法会把注入的同一个token的请求方法一并取消掉 // 上面的get和post请求都会被取消掉 // cancel the request (the message parameter is optional) source.cancel('Operation canceled by the user.'); 从 demo 上来看,用法很简单,token 和 cancel 的关系对应上即可。官方例子中还有一种取消请求的方式,这个我们放在后面讲,更容易理解一些。 2. 源码解析 # 取消请求的方法在 https://github.com/axios/axios/tree/master/lib/cancel 的目录中,3 个文件: Cancel: Cancel 类,message 和__CANCEL__属性,用于标识取消的某个请求; isCancel: 判断当前参数是否是 Cancel 的实例; CancelToken: 主流程,创建 Cancel 实例和取消的方法; 我们主要来看下 CancelToken 中的整个流程。 2.1 source 方法 # source 作为取消请求的入口,我们就先来看下 source 方法。// 创建token和cancel方法 CancelToken.source = function source() { var cancel; // token为 CancelToken 的实例,包含 promise 和 reason 两个属性 // 同时把 executor 中的参数给到 cancel // 即CancelToken有一个回调函数,而这个回调函数的参数也是一个函数 // CancelToken怎么执行,我们接着看! var token = new CancelToken(function executor(c) { cancel = c; }); return { token: token, cancel: cancel, }; }; 2.2 CancelToken # CancelToken 用来取消请求,但我理解起来,思路非常的绕,我们一点点来剖析:function CancelToken(executor) { if (typeof executor !== 'function') { throw new TypeError('executor must be a function.'); } // 创建一个Promise的实例, // 当resolvePromise执行时,this.promise变为fulfilled状态 var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); // new一个实例时,会立即执行CancelToken的回调函数executor方法 // executor的参数也是一个函数,即上面的cancel就是当前的cancel函数体 // 当executor的回调函数cancel执行时,会给当前CancelToken创建一个reason属性,这个属性是Cancel的实例 // 并执行resolvePromise方法,将reason实例穿进去;执行后this.promise变为fulfilled状态 var token = this; executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested return; } token.reason = new Cancel(message); resolvePromise(token.reason); }); } 也就是说会先创建一个 CancelToken 的实例 token,同时,将 CancelToken 中回调函数的参数给到了 cancel。当 cancel 执行时,则 token 中的 promise 属性则会从 pending 状态变为 fulfilled 状态,那么 promise 上挂载的then()方法也就可以继续执行了。 2.3 adapter # 在调用 cancel 方法后,请求中是怎么操作的呢?我们看下adapter/xhr.js中的代码:if (config.cancelToken) { // config.cancelToken就是上面创建的token // 当token.promise变为fulfilled状态后,就可以执行后续的链式操作 // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } // 取消当前的请求 request.abort(); // 将Cancel的实例cancel给到reject reject(cancel); // Clean up request request = null; }); } 当我们使用 axios 的 catch 捕获内部抛出的异常时,就可以通过isCancel判断是否是因主动取消请求导致的异常:axios .get('/user/12345', { cancelToken: source.token, // 将token注入到请求中 }) .catch(function (thrown) { // 判断是否是因主动取消导致的 if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // handle error console.error(thrown); } }); 现在再来看下 cancel 执行的整个流程,就会清晰流畅很多。 3. 取消请求的另一种方式 # 我们在第 1 节还留着一个问题,axios 取消请求还有另一种方式,即直接使用 CancelToken 类。 token: 我们在上面的 source()方法中就能看到,传给 axios 参数的 token 就是 CancenToken 的实例,这里直接使用new CancelToken()的返回值也是可以的; cancel: source()中的 cancel 就是 CancelToken 的回调函数 executor 的回调函数; const CancelToken = axios.CancelToken; let cancel; axios.get('/user/12345', { // CancelToken创建的 cancelToken: new CancelToken(function executor(c) { // An executor function receives a cancel function as a parameter cancel = c; }), }); // cancel the request cancel(); 其实我们发现,source()方法,只是给我们额外又封装了一下,简单的返回了 token 和 cancel,但本质还是 CancelToken 中的东西。 4. 总结 # 在取消请求的过程中,token 要和 cancel 方法保持对应关系,即都在一个对象里;若其他的请求也要取消时,可以额外再生成一组 token 和 cancel。同时,这里还用到了 Promise 的一个机制,只有在当前 Promise 变更为 fulfilled 状态后,才能执行后面的 then 等操作。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
别再搞博客了,赶快写吧
之前看很多人的文章都是自己如何搭建的博客,无论是 wordpress,还是静态的 jekyll, hexo,hugo 等,还是现在的 node 直出,vuepress 等等,各种搭建博客系统的博客。即使是使用第三方的博客平台(如博客园、掘金、csdn 等),也要把界面和功能改造一番。有的人还不过瘾,还在各种博客技能和多个第三方博客平台上,反复横跳。最后发现他并没有写几篇实在的文章,满心思都在优化系统上了。例如: 折腾累了就换 Wordpress 了,不过最近又打算自己从头写一套了; 一开始用 jekyll,后来到 WordPress,再后来到 typecho,然后 mediawiki、dokuwiki,ghost 诞生了!切换到 ghost,切回 WordPress,开始使用 hexo,vue.js 来了开始使用 vuepress,Hugo ! 从 hexo 折腾到 ghost,后来不想折腾了就用 wordpress; 各种折腾博客的人与事,然后,折腾着,折腾着,博客就没了。 搭建博客的教程,从网上能搜出无数个结果,但用什么博客系统重要吗? 不要再折腾 # 做为一个写了 10 年博客的人,博客系统使用真的不怎么重要。例如司徒正美就在博客园上写了好多年的博客,张鑫旭是在 wordpress 上耕耘了好多年,我本人从 2014 年用 php 搭建了博客系统之后,也基本没怎么改动过了。上半年时,我本想把博客从 php 改造成 node 技术栈,后来在一番心理斗争,两分钟,两分钟后,我放弃了这个想法。我们搭建博客的目的,是为了更好地写博客,写的博客文章才是重点,不要本末倒置了。而且,现在的博客系统又不是不能用。也有的人说搭建博客,是想锻炼自己的能力,这也能算说的过去。向着能使用当前最流行的技术搭建一套博客系统出来,然后进行总结。例如早期的 java\php 博客系统,到后来的 hexo, hugo 静态博客生成器,还有现在前端工程师喜欢搞的 node 直出博客系统等等。你搭建博客系统的过程,也能沉淀下来东西,要再形成博客文章,就更好了。如果只是闲不住,我劝,年轻人,耗子尾汁。对于想写文章的人,我有几个建议: 不想折腾,只想写文章的,推荐使用成熟的博客平台,不用关心博客,专心码字,同时索引也很快,阅读量会比较多,也有相应的评论系统,例如博客园、知乎、掘金等;而且现在比如语雀、飞书文档等也不错,也可以用来写文章; 想有自己博客的,首推静态博客,可以自定义主题,例如 hexo, hugo, vuepress 等,搭配着 git 命令,发布也很方便,自动构建,托管在免费的 GitHub 上,但评论上欠缺一些; 其次一些成熟的博客也很不错,如 wordpress。 最想折腾的,从前端,到后端,再到数据库,都想着自己一手操控的,那就从零搭建吧; 如何写好博客 # 这个问题也有很多的讨论了!首先就是要确定当前文章的主题,然后划分目录,每个目录讲什么内容,是否要配备 demo 样例。我博客文章主题的灵感,通常来源于工作中,例如工作遇到的重点难点,一个比较酷炫的功能我是怎样实现的,使用的这个第三方组件内部原理是什么,等等,现在没时间写关系,这个转瞬即逝的想法要记录下来,并写几个关键点,等有空的时候,可以进行深入研究,并形成博客文章。例如这些是我在企业微信上创建的代办事项,一个个完成后,就变成了已完成状态。 你的写作坚持多久了 # 就不说个人博客吧,无论您是在哪个地方写作的,您到现在坚持有几年了呢?我想,您现在应该试试,再重新开始写起来。
2024年10月20日
3 阅读
0 评论
0 点赞
1
...
41
42
43
...
213