事实:迅步:关键词优化报价,不花冤枉钱!
优采云 发布时间: 2022-10-13 06:16事实:迅步:关键词优化报价,不花冤枉钱!
关键词优化报价在业内已不是什么秘密,但如何做出合理的关键词优化报价却是每一个外包公司都难以做到的。信任等因素,使得这个行业的报价看不懂或看不懂是个通病。今天我们将重点介绍关键词优化报价的几个方向和思路,帮助甲方老板深入了解关键词@关键词优化报价背后的故事。
让我们回顾一下网站优化的过程。优化关键词是如何实现的?
1.关键词优化过程
我们都知道,在关键词优化的过程中,最重要的核心是找到关键词,那么关键词不能是broad、broad、large,没有实际意义。我们的关键词你所做的没有任何意义,比如“操作”关键词,虽然这个词也有一定的搜索量,但是对于用户来说,搜索操作并不知道用户的真实意图,例如用户搜索“操作是要做的”。什么?”,用户的意图是通过搜索这个词来了解操作相关的知识,所以我们尝试做一些可搜索的长尾词而不是核心词。找到词库后,我们需要整理出对应的长尾词-tail words. 词库放入EXECL,然后由网站优化,将长尾词放在<
接下来,我们需要从内容上优化网站。我们可以通过在标题和文章主要内容中添加长尾词+相关词来突出主要内容。需要在主要内容中添加一些锚文本,以吸引蜘蛛抓取 网站 内容。
最后通过发布文章再进行关键词优化,这就是一个完整的关键词优化过程。整个过程可以分为word digging-layout-publishing文章来实现。
2、关键词优化报价如何计算?
那么在了解了关键词优化过程之后,接下来我们就可以了解一下关键词优化报价是如何计算的了?从上面的过程我们知道,一个长尾词的优化难度小于核心词,所以我们需要围绕核心词展开长尾词。例如,我们可以通过 10 个长尾词 文章 词 文章 链接到核心,这样就可以对核心词进行排名。这样一来,基本就需要看核心词的难度了。一般情况下,指数低于500,说明竞争不激烈,通过10个-20左右的长尾词文章可以做核心词,但指数超过500后,竞争激烈,而关键词的排名也不是那么容易做到的,像市场上的许多优化报价基于 关键词 是通过快速排序计算的。一般一个长尾词只需要10-20元,但是这种情况下,长尾词一般没有搜索量,所以很容易做Up,但是一旦核心词索引比较高,这个成本就是不容易计算。
总之,我们在做关键词优化报价的时候,基本上如果用指定的词来做排名,价格是不固定的,因为没有参考标准,也就是说很多外包公司随意要价,而且没有参考标准,因为他不知道要花多少钱才能在首页排名,所以业内的关键词优化报价其实是一个不规范的报价,看看就好,别'不要当真,如果想了解更多相关知识,关注寻布。
总结:【第2167期】埋点自动收集方案-路由依赖分析
前言
前段时间看到有相关需求,不过是某个团队开发的。今日前端早读班文章由转转@刁文浩分享,公众号:大转转FE。
@DIAO文豪,目前就职于转转平台运营部,负责C2C业务线的前端工作。他曾参与过两家初创公司,并担任过项目经理。他喜欢探索业务痛点。同时,他热爱技术研究。致力于通过技术手段提升产品和用户体验,促进业务增长。
正文从这里开始~~
让我们考虑一下这两个问题。
1、一共有多少个项目?每个页面收录多少个组件?
2. 哪些组件是公共组件,它们被哪些页面引用?
跟着这个文章一起来讨论,希望能帮助你找到答案。
背景
由于组件化的思想深入人心,当我们在开发中遇到具体的功能模块或UI模块时,就会想到提取组件。更高级的方法是将多个页面的相似部分提取到公共组件中。
组件化的“诅咒”
但往往对一件事的依赖越强,就越容易落入它的“魔咒”。当项目的组件比较多的时候,开发者更难建立它们之间的关系,尤其是当一个组件的一行代码发生变化时,甚至无法准确判断哪些页面会受到这一行变化的影响的代码。我暂时称它为“组件化的诅咒”。如果我们有完整的组件依赖,就可以很好的解决这个问题。
让我们以下面的场景为例,来看看依赖分析的重要性和必要性。
在整个方案中,埋点的数据源非常重要,数据源与页面的对应关系是保证数据源完整性的关键。比如首页和个人首页的产品流使用同一张产品卡片,开发者自然会将产品卡片分离成一个通用组件。如下:
<p>//Index.vue 首页
import Card from './common/Card.vue' //依赖商品卡片组件
<br />
//Home.vue 个人主页
import Card from './common/Card.vue' //依赖商品卡片组件
<br />
//Card.vue 商品卡片组件
goDetail(item) {
/**
* @mylog 商品卡片点击
*/
this.$log('card-click') // 埋点发送
}</p>
这就带来了一个问题:产品卡片的点击信息(埋点的数据来源)可能在首页,也可能在个人主页,而jsdoc在采集埋点注解时,无法判断归属。所以我们必须想办法得到组件和页面之间的映射关系。
想要的效果
项目中的实际依赖:
对应的依赖分析关系:(每个组件,与引用它的页面路由的映射)
程序思维
那么,如何进行依赖分析呢?在考虑这个之前,让我们看一下构建依赖项的一些常用语法。
<p>//a.ts
import B from './b.ts'
import getCookie from '@/libs/cookie.ts'
<br />
//c.ts
const C = require('./b.ts')
<br />
//b.ts
div {
background: url('./assets/icon.png') no-repeat;
}
import './style.css'
// c.vue
import Vue from Vue
import Card from '@/component/Card.vue'</p>
以下是依赖分析的三个想法:
递归
从项目的路由配置开始,递归解析每个路由页面的依赖关系。这个思路简单直接,但是实现起来可能比较麻烦,页面中各种形式的依赖都需要解决。
借助工具的统计分析数据,进行二次加工
在实际项目中,我们都使用 webpack 打包工具,它的一大特色就是会自动帮助开发者做依赖分析(独立的增强解析库)。相比第一种重写解析的方法,何不站在 webpack 的肩膀上解决问题。
我们先来看看webpack的整体编译过程:
可以看到,每个文件都会经过resolve阶段,最后在编译完成后,会得到本次编译的统计分析信息。
<p>//done是compiler的钩子,在完成一次编译结束后的会执行
compiler.hooks.done.tapAsync("demoPlugin",(stats,cb)=>{
fs.writeFile(appRoot+'/stats.json', JSON.stringify(stats.toJson(),'','\t'), (err) => {
if (err) {
throw err;
}
})
cb()
})</p>
详细的编译数据是 done 事件中的回调参数 stats。处理后大致如下:
通过对这个统计分析信息的二次处理和分析,也可以得到预期的依赖关系(插件webpack-bundle-analyzer也可以根据这个数据生成分析图表)。这些数据看起来更像是对基本块和模块的依赖分析。对于组件或通用组件的依赖关系,需要对chunk和modules进行综合分析来解决。同时我们也发现这个数据的数据量相当大,还有很多开发者不关心的数据(截图是只有两个路由页面时的数据量)。接下来讨论的方案是作者实际采用的方案,也是基于webpack,区别在于分析和采集依赖的时机。
在webpack的解析阶段,分析采集依赖
我们看到,虽然 webpack 的分析数据非常臃肿,但它确实帮助开发人员完成了繁重的工作。只是我们希望自定义数据的范围,主动采集想要的数据,所以推测是否可以在每个文件解析阶段进行一定的“干预”,即通过条件判断或者过滤来达到目的. 那么问题来了,在解决的哪个阶段我们应该“干预”,如何“干预”?
好的,我们先来概述一下 webpack 事件流流程:
显然,afterResolve 是每个文件解析阶段的结束,应该从这里开始。
执行
先提供流程图
初始化
首先,这是一个 webpack 插件。在初始化阶段,指定解析的路由文件的地址(如src/route)和排除文件的地址(如src/lib、src/util),因为这些排除的文件不会被掩埋。点数据。
采集依赖项
在 afterResolve 钩子函数中,获取当前解析文件的路径及其父文件路径。
<p>apply(compiler) {
compiler.hooks.normalModuleFactory.tap(
"demoPlugin",
nmf => {
nmf.hooks.afterResolve.tapAsync(
"demoPlugin",
(result, callback) => {
const { resourceResolveData } = result;
// 当前文件的路径
let path = resourceResolveData.path;
// 父级文件路径
let fatherPath = resourceResolveData.context.issuer;
callback(null,result)
}
);
}
)
}</p>
构建依赖树
根据上一步得到的引用关系生成依赖树。
<p>// 不是nodemodule中的文件,不是exclude中的文件,且为.js/.jsx/.ts/.tsx/.vue
if(!skip(this.ignoreDependenciesArr,this.excludeRegArr,path, fatherPath) && matchFileType(path)){
if(fatherPath && fatherPath != path){ // 父子路径相同的排除
if(!(fatherPath.endsWith('js') || fatherPath.endsWith('ts')) || !(path.endsWith('js') || path.endsWith('ts'))){
// 父子同为js文件,认为是路由文件的父子关系,而非组件,故排除
let sonObj = {};
sonObj.type = 'module';
sonObj.path = path;
sonObj.deps = []
// 如果本次parser中的path,解析过,那么把过去的解析结果copy过来。
sonObj = copyAheadDep(this.dependenciesArray,sonObj);
let obj = checkExist(this.dependenciesArray,fatherPath,sonObj);
this.dependenciesArray = obj.arr;
if(!obj.fileExist){
let entryObj = {type:'module',path:fatherPath,deps:[sonObj]};
this.dependenciesArray.push(entryObj);
}
}
} else if(!this.dependenciesArray.some(it => it.path == path)) {
// 父子路径相同,且在this.dependenciesArray不存在,认为此文件为依赖树的根文件
let entryObj = {type:'entry',path:path,deps:[]};
this.dependenciesArray.push(entryObj);
}
}</p>
那么生成的依赖树如下:
解析路由信息
通过上一步,我们基本上得到了组件的依赖树,但是我们发现对于公共组件Card,它只存在于首页的依赖中,而没有存在于个人首页的依赖中,这显然是不符合预期(在步骤 6 中解释)。那么接下来就是找到这个依赖树和路由信息的关系了。
<p>compiler.hooks.done.tapAsync("RoutePathWebpackPlugin",(stats,cb)=>{
this.handleCompilerDone()
cb()
})</p>
<p>// ast解析路由文件
handleCompilerDone(){
if(this.dependenciesArray.length){
let tempRouteDeps = {};
// routePaths是项目的路由文件数组
for(let i = 0; i it && Object.prototype.toString.call(it) == "[object Object]" && it.components);
// 获取真实插件传入的router配置文件的依赖,除去main.js、filter.js、store.js等文件的依赖
this.dependenciesArray =
getRealRoutePathDependenciesArr(this.dependenciesArray,this.routePaths);
}
}</p>
通过ast解析这一步,可以得到如下路由信息:
<p>[
{
"name": "index",
"route": "/index",
"title": "首页",
"components": ["../view/newCycle/index.vue"]
},
{
"name": "home",
"route": "/home",
"title": "个人主页",
"components": ["../view/newCycle/home.vue"]
}
]</p>
集成依赖树和路由信息分析
<p>// 将路由页面的所有依赖组件deps,都存放在路由信息的components数组中
const getEndPathComponentsArr = function(routeDeps,dependenciesArray) {
for(let i = 0; i {
routeDeps = routeDeps.map(routeObj=>{
if(routeObj && routeObj.components){
let relativePath =
routeObj.components[0].slice(routeObj.components[0].indexOf('/')+1);
if(page.path.includes(relativePath.split('/').join(path.sep))){
// 铺平依赖树的层级
routeObj = flapAllComponents(routeObj,page);
// 去重操作
routeObj.components = dedupe(routeObj.components);
}
}
return routeObj;
})
})
}
return routeDeps;
}
//建立一个map数据结构,以每个组件为key,以对应的路由信息为value
// {
// 'path1' => Set { '/index' },
// 'path2' => Set { '/index', '/home' },
// 'path3' => Set { '/home' }
// }
const convertDeps = function(deps) {
let map = new Map();
......
return map;
}</p>
综合分析后的依赖关系如下:
<p>{
A: ["index&_&首页&_&index"],// A代表组件A的路径
B: ["index&_&首页&_&index"],// B代表组件B的路径
Card: ["index&_&首页&_&index"],
// 映射中只有和首页的映射
D: ["index&_&首页&_&index"],// D代表组件D的路径
E: ["home&_&个人主页&_&home"],// E代表组件E的路径
}</p>
因为上一步的依赖采集部分,Card组件没有成功采集到个人主页的依赖,所以这一步综合分析无法建立准确的映射关系。请参阅下面的解决方案。
修改 unsafeCache 配置
为什么公共组件 Card 在采集依赖时只采集一次?如果这个问题不解决,就意味着只采集首页的产品点击,其他引用该组件的页面产品点击会丢失。哪里有问题,哪里就有机会,而机会就是解决问题的可能性。
webpack4 为解析提供了一个配置入口。开发者可以通过几个设置来决定如何解析文件,比如扩展名、别名等。有一个属性——unsafeCache成功引起了笔者的注意,这就是问题的根源。
UnsafeCache 是 webpack 用来提高编译性能的优化措施。
unsafeCache 的默认值为 true,表示 webpack 会缓存解析后的文件依赖。当需要再次解析文件时,会直接从缓存中返回结果,避免重复解析。
让我们看一下源代码:
<p>//webpack/lib/WebpackOptionsDefaulter.js
this.set("resolveLoader.unsafeCache", true);
//这是webpack初始化配置参数时对unsafeCache的默认设置
<br />
//enhanced-resolve/lib/Resolverfatory.js
if (unsafeCache) {
plugins.push(
new UnsafeCachePlugin(
"resolve",
cachePredicate,
unsafeCache,
cacheWithContext,
"new-resolve"
)
);
plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));
} else {
plugins.push(new ParsePlugin("resolve", "parsed-resolve"));
}
//前面已经提到,webpack将文件的解析独立为一个单独的库去做,那就是enhanced-resolve。
//缓存的工作是由UnsafeCachePlugin完成,代码如下:
//enhanced-resolve/lib/UnsafeCachePlugin.js
apply(resolver) {
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("UnsafeCachePlugin", (request, resolveContext, callback) => {
if (!this.filterPredicate(request)) return callback();
const cacheId = getCacheId(request, this.withContext);
// !!划重点,当缓存中存在解析过的文件结果,直接callback
const cacheEntry = this.cache[cacheId];
if (cacheEntry) {
return callback(null, cacheEntry);
}
resolver.doResolve(
target,
request,
null,
resolveContext,
(err, result) => {
if (err) return callback(err);
if (result) return callback(null, (this.cache[cacheId] = result));
callback();
}
);
});
}</p>
UnsafeCachePlugin的apply方法中,当判断有缓存文件结果时,直接回调,不再继续后续的解析动作。
这对我们的依赖集合有何影响?
解析后的文件是缓存的,也就是说当再次遇到该文件时,事件流会提前终止,afterResolve钩子自然不会被执行,所以我们的依赖也就无从谈起了。
其实webpack的resolve过程可以看成是事件的串联。当所有连接的事件都被执行时,解析就结束了。我们来看看原理:
用于解析文件的库是增强解析。Resolverfactory在生成resolver解析对象时,注册了大量的插件,正是这些插件构成了一系列的解析事件。
<p>//enhanced-resolve/lib/Resolverfatory.js
exports.createResolver = function(options) {
......
let unsafeCache = options.unsafeCache || false;
if (unsafeCache) {
plugins.push(
new UnsafeCachePlugin(
"resolve",
cachePredicate,
unsafeCache,
cacheWithContext,
"new-resolve"
)
);
plugins.push(new ParsePlugin("new-resolve", "parsed-resolve"));
// 这里的事件流大致是:UnsafeCachePlugin的事件源(source)是resolve,
//执行结束后的目标事件(target)是new-resolve。
//而ParsePlugin的事件源为new-resolve,所以事件流机制刚好把这两个插件串联起来。
} else {
plugins.push(new ParsePlugin("resolve", "parsed-resolve"));
}
...... // 各种plugin
plugins.push(new ResultPlugin(resolver.hooks.resolved));
<br />
plugins.forEach(plugin => {
plugin.apply(resolver);
});
<br />
return resolver;
}</p>
每个插件执行完自己的逻辑后,会调用resolver.doResolve(target, ...),其中target为触发下一个插件的事件名称,以此类推,直到事件源为result,递归终止,并解决完成。
resolve的事件链流程图大致如下:
UnsafeCachePlugin插件第一次解析文件时,由于没有缓存,会触发target为new-resolve的事件,即ParsePlugin,并将解析结果记录到缓存中。当判断文件有缓存结果时,UnsafeCachePlugin的apply方法会直接回调而不继续执行resolver.doResolve(),也就是说整个resolve事件流在UnsafeCachePlugin中终止。这就解释了为什么只建立了首页与Card组件的映射,却无法获取到个人首页与Card组件的映射。
解决方案
分析了原因,很简单,把unsafeCache设置为false(嗯,就这么简单)。这时候你可能会担心项目编译速度会变慢,但是再仔细想想,依赖分析完全可以独立于开发阶段,只要我们在需要的时候执行这个能力,比如由开发者通过命令行参数来控制。
<p>//package.json
"analyse": "cross-env LEGO_ENV=analyse vue-cli-service build"
<br />
//vue.config.js
chainWebpack(config) {
// 这一步解决webpack对组件缓存,影响最终映射关系的处理
config.resolve.unsafeCache = process.env.LEGO_ENV != 'analyse'
}</p>
最终依赖
<p>{
A: ["index&_&首页&_&index"],// A代表组件A的路径
B: ["index&_&首页&_&index"],// B代表组件B的路径
Card: ["index&_&首页&_&index",
"home&_&个人主页&_&home"],
// Card组件与多个页面有映射关系
D: ["index&_&首页&_&index"],// D代表组件D的路径
E: ["home&_&个人主页&_&home"],// E代表组件E的路径
}</p>
可以看到,个人主页的路由信息被添加到与公共组件Card关联的映射页面中,就是准确的依赖数据。在埋点自动采集项目中,通过jsdoc处理这个依赖数据,就可以完成所有埋点信息与页面的映射关系。
还有一件事
webpack5,它来了,它带有持久缓存策略。前面提到的unsafeCache虽然可以提高应用构建性能,但是牺牲了一定的分辨率精度。同时,这意味着连续构建过程需要反复重启决策策略,这就需要采集文件搜索策略(resolutions)的变化。, 识别和判断文件分辨率是否发生变化,这一系列的过程也是有代价的,所以才叫unsafeCache,而不是safeCache(安全)。
webpack5 在配置信息中指定缓存对象的类型,可以设置为内存和文件系统。memory 指的是之前的 unsafeCache 缓存,fileSystem 指的是相对安全的磁盘持久缓存。
<p>module.exports = {
cache: {
// 1. Set cache type to filesystem
type: 'filesystem',
<br />
buildDependencies: {
// 2. Add your config as buildDependency to get cache invalidation on config change
config: [__filename]
<br />
// 3. If you have other things the build depends on you can add them here
// Note that webpack, loaders and all modules referenced from your config are automatically added
}
}
};</p>
所以对于webpack5来说,如果需要做完整的依赖分析,只需要动态设置cache.type为memory,resolve.unsafeCache为false即可。(有兴趣的童鞋可以试一试)
总结
上面我们讲解了组件化可能存在的隐患,提到了路由依赖分析的重要性,给出了依赖分析的三个思路,并重点介绍了其中一种基于埋点自动采集的解决方案的具体实现。在这里与大家分享,期待共同成长~