Compare commits
No commits in common. "master" and "v1.1.0" have entirely different histories.
@ -3,8 +3,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="text-align:center">
|
<p style="text-align:center">
|
||||||
<img src="https://img.shields.io/github/repo-size/colorfulsweet/blog-web.svg" alt="Size" />
|
<img src="https://img.shields.io/github/repo-size/sookie2010/hexo_blog.svg" alt="Size" />
|
||||||
<img src="https://img.shields.io/github/package-json/v/colorfulsweet/blog-web.svg" alt="Version" />
|
<img src=https://img.shields.io/github/package-json/v/sookie2010/hexo_blog.svg alt="Version" />
|
||||||
<a target="_blank" href="https://www.colorfulsweet.site">
|
<a target="_blank" href="https://www.colorfulsweet.site">
|
||||||
<img src="https://img.shields.io/badge/blog-colorfulsweet-green.svg" alt="我的博客" />
|
<img src="https://img.shields.io/badge/blog-colorfulsweet-green.svg" alt="我的博客" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
10
_config.yml
@ -36,8 +36,7 @@ skip_render:
|
|||||||
new_post_name: :title.md # File name of new posts
|
new_post_name: :title.md # File name of new posts
|
||||||
default_layout: post
|
default_layout: post
|
||||||
titlecase: false # Transform title into titlecase
|
titlecase: false # Transform title into titlecase
|
||||||
external_link:
|
external_link: true # Open external links in new tab
|
||||||
enable: true # Open external links in new tab
|
|
||||||
filename_case: 0
|
filename_case: 0
|
||||||
render_drafts: false
|
render_drafts: false
|
||||||
post_asset_folder: false
|
post_asset_folder: false
|
||||||
@ -94,15 +93,12 @@ pagination_dir: page
|
|||||||
theme: yilia
|
theme: yilia
|
||||||
|
|
||||||
# 图片存储仓库地址
|
# 图片存储仓库地址
|
||||||
picture_cdn: https://nas.colorfulsweet.site:9003
|
picture_cdn: https://blog-cdn.nos-eastchina1.126.net
|
||||||
|
|
||||||
# ICP备案号
|
|
||||||
ICP: 鲁ICP备19028444号
|
|
||||||
|
|
||||||
# 站点地图
|
# 站点地图
|
||||||
sitemap:
|
sitemap:
|
||||||
path: sitemap.xml
|
path: sitemap.xml
|
||||||
template: ./templates/sitemap.xml
|
template: ./sitemap_template.xml
|
||||||
baidusitemap:
|
baidusitemap:
|
||||||
path: baidusitemap.xml
|
path: baidusitemap.xml
|
||||||
|
|
||||||
|
|||||||
@ -1,25 +1,22 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
class Deploy {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
* 发布静态化的站点
|
* 发布静态化的站点
|
||||||
* @param {String} source 源位置
|
* @param {String} source 源位置
|
||||||
* @param {String} target 目标位置
|
* @param {String} target 目标位置
|
||||||
* @param {Boolean} isRemove 是否先进行删除
|
* @param {Boolean} copyRoot 是否拷贝根目录
|
||||||
*/
|
*/
|
||||||
async exec(source, target, isRemove = false) {
|
async exec(source, target, copyRoot) {
|
||||||
if(isRemove) {
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
console.log(`删除${target}目录中的文件`)
|
console.log(`删除${target}目录中的文件`)
|
||||||
this._deleteFolderRecursive(target, true)
|
this._deleteFolderRecursive(target, true)
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
console.log(`拷贝${source}所有文件 -> ${target}`)
|
console.log(`拷贝${source}所有文件 -> ${target}`)
|
||||||
this._checkDirectory(target)
|
this._copyFolderRecursive(source, target, copyRoot)
|
||||||
this._copyFolderRecursive(source, target)
|
},
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 递归删除目录以及子目录中的所有文件
|
* 递归删除目录以及子目录中的所有文件
|
||||||
@ -38,7 +35,7 @@ class Deploy {
|
|||||||
if(!retainRoot) { // 根目录保留
|
if(!retainRoot) { // 根目录保留
|
||||||
fs.rmdirSync(curPath)
|
fs.rmdirSync(curPath)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
/**
|
/**
|
||||||
* 递归拷贝目录
|
* 递归拷贝目录
|
||||||
* @param {String} source 源位置
|
* @param {String} source 源位置
|
||||||
@ -56,28 +53,24 @@ class Deploy {
|
|||||||
let writable = fs.createWriteStream(_target) //创建写入流
|
let writable = fs.createWriteStream(_target) //创建写入流
|
||||||
readable.pipe(writable);
|
readable.pipe(writable);
|
||||||
} else if (stats.isDirectory()) { //是目录则 递归
|
} else if (stats.isDirectory()) { //是目录则 递归
|
||||||
this._checkDirectory(_target, this._copyFolderRecursive, _src, _target)
|
this._checkDirectory(_src, _target, this._copyFolderRecursive)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验目标目录是否存在
|
* 校验目标目录是否存在
|
||||||
|
* @param {String} src 源目录
|
||||||
* @param {String} target 目标目录
|
* @param {String} target 目标目录
|
||||||
* @param {Function} callback 回调函数
|
* @param {Function} callback 回调函数
|
||||||
* @param {Array} args 回调函数入参
|
|
||||||
*/
|
*/
|
||||||
_checkDirectory (target, callback, ...args) {
|
_checkDirectory (src,target,callback) {
|
||||||
fs.access(target, fs.constants.F_OK, err => {
|
fs.access(target, fs.constants.F_OK, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
fs.mkdirSync(target)
|
fs.mkdirSync(target)
|
||||||
}
|
}
|
||||||
if (typeof callback === 'function') {
|
callback.call(this, src, target)
|
||||||
callback.apply(this, args)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new Deploy()
|
|
||||||
136
deploy_utils/image_synchronize.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
const fs = require('fs'),
|
||||||
|
path = require('path'),
|
||||||
|
nos = require('@xgheaven/nos-node-sdk')
|
||||||
|
|
||||||
|
class ImageSynchronizer {
|
||||||
|
/**
|
||||||
|
* 构造方法
|
||||||
|
* @param {Object} setting NosClient的设置项
|
||||||
|
* @param {Array} imagesList 本地图片的列表
|
||||||
|
* @param {String} rootPath 本地文件根路径
|
||||||
|
*/
|
||||||
|
constructor(setting, imagesList, rootPath) {
|
||||||
|
// 网易云对象存储调用接口client
|
||||||
|
this.client = new nos.NosClient(setting)
|
||||||
|
this.imagesList = imagesList
|
||||||
|
this.rootPath = rootPath
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 执行文件同步
|
||||||
|
* @param {String} prefix 图片目录前缀
|
||||||
|
*/
|
||||||
|
synchronize(prefix) {
|
||||||
|
return this._queryObjects({limit: this.imagesList.length+1, prefix}, function(pendingUploadFiles){
|
||||||
|
this._uploadObject(pendingUploadFiles)
|
||||||
|
}, function(pendingDeleteFiles){
|
||||||
|
this._deleteObjects(pendingDeleteFiles)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 查询所有对象存储库已存在的文件
|
||||||
|
* @param {Object} params 查询的参数
|
||||||
|
* @param {Function} uploadCallback 处理待上传文件的回调函数
|
||||||
|
* @param {Function} deleteCallback 处理待删除文件的回调函数
|
||||||
|
*/
|
||||||
|
async _queryObjects(params, uploadCallback, deleteCallback) {
|
||||||
|
// 列出所有已存储的对象
|
||||||
|
const ret = await this.client.listObject(params)
|
||||||
|
// ret 包括 items(元素),limit(请求的数量),nextMarker(下一个标记)
|
||||||
|
let storageItems = ret.items.filter(item => {
|
||||||
|
return /^images.+?\.(png|jpe?g|gif)$/.test(item.key)
|
||||||
|
}).sort((item1, item2) => {
|
||||||
|
if (item1.key > item2.key) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
else if (item1.key < item2.key) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
});
|
||||||
|
// 待上传的文件列表
|
||||||
|
let pendingUploadFiles = this.imagesList.filter(item => {
|
||||||
|
let index = this._binarySearch(storageItems, item.name, 'key', 0, storageItems.length - 1)
|
||||||
|
if (index === -1) {
|
||||||
|
// 文件名不存在, 代表是新文件
|
||||||
|
item.type = 'new'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else if (storageItems[index].eTag !== item.md5) {
|
||||||
|
// 文件名存在, 但是hash值不同, 代表有变化
|
||||||
|
item.type = 'change'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
});
|
||||||
|
// 处理待上传的文件
|
||||||
|
uploadCallback.call(this, pendingUploadFiles);
|
||||||
|
// 待删除的文件列表( 仓库中存在, 本地不存在 )
|
||||||
|
let pendingDeleteFiles = storageItems.filter(item => {
|
||||||
|
return this._binarySearch(this.imagesList, item.key, 'name', 0, this.imagesList.length - 1) === -1;
|
||||||
|
})
|
||||||
|
// 处理待删除的文件
|
||||||
|
deleteCallback.call(this, pendingDeleteFiles.map(item => item.key))
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 上传文件对象
|
||||||
|
* @param {Array} filesList 待上传的文件列表
|
||||||
|
* @param {Number} index 索引值
|
||||||
|
*/
|
||||||
|
_uploadObject(filesList, index=0) {
|
||||||
|
if(index >= filesList.length) return
|
||||||
|
|
||||||
|
this.client.putObject({
|
||||||
|
objectKey: filesList[index].name,
|
||||||
|
body: fs.createReadStream(path.resolve(this.rootPath, filesList[index].name)), // 支持 Buffer/Readable/string
|
||||||
|
}).then(result => {
|
||||||
|
// eTag是上传后远端校验的md5值, 用于和本地进行比对
|
||||||
|
let eTag = result.eTag.replace(/"/g,'')
|
||||||
|
if(filesList[index].md5 === eTag) {
|
||||||
|
console.log(`${filesList[index].name} 上传成功, md5:${eTag} 类型: ${filesList[index].type}`)
|
||||||
|
} else {
|
||||||
|
console.warn(`${filesList[index].name} 上传出错, md5值不一致`)
|
||||||
|
console.warn(`===> 本地文件: ${filesList[index].md5}, 接口返回: ${eTag}`)
|
||||||
|
}
|
||||||
|
this._uploadObject(filesList, ++index)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 批量删除文件
|
||||||
|
* @param {Array} fileNamesList 文件名数组
|
||||||
|
*/
|
||||||
|
_deleteObjects(fileNamesList) {
|
||||||
|
if(!Array.isArray(fileNamesList) || !fileNamesList.length) return
|
||||||
|
|
||||||
|
this.client.deleteMultiObject({
|
||||||
|
objectKeys: fileNamesList
|
||||||
|
}).then(err => {
|
||||||
|
console.log('===> 文件删除成功')
|
||||||
|
fileNamesList.forEach(item => console.log(item))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 二分法查找
|
||||||
|
* @param {Array} arr 执行查找的数组
|
||||||
|
* @param {Object} target 要找到的目标元素
|
||||||
|
* @param {String} key 数组元素上的键
|
||||||
|
* @param {Number} start 查找的范围 起点
|
||||||
|
* @param {Number} end 查找的范围 终点
|
||||||
|
*/
|
||||||
|
_binarySearch(arr, target, key, start, end) {
|
||||||
|
if(!Array.isArray(arr) || !arr.length) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if(start >= end) {
|
||||||
|
return arr[start][key] === target ? start : -1
|
||||||
|
}
|
||||||
|
let index = Math.ceil((start + end)/2)
|
||||||
|
if(arr[index][key] === target) {
|
||||||
|
return index
|
||||||
|
} else if(arr[index][key] > target) {
|
||||||
|
return this._binarySearch(arr, target, key, start, index-1)
|
||||||
|
} else {
|
||||||
|
return this._binarySearch(arr, target, key, index+1, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = ImageSynchronizer
|
||||||
52
deploy_utils/list_images.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
|
||||||
|
function sortName(item1, item2) {
|
||||||
|
if(item1.name > item2.name) {
|
||||||
|
return 1
|
||||||
|
} else if(item1.name < item2.name) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归遍历目录中的所有文件
|
||||||
|
* @param {String} imageFolderPath 文件夹路径
|
||||||
|
* @param {Array} images 图片列表
|
||||||
|
* @param {String} rootPath 根路径
|
||||||
|
*/
|
||||||
|
function readDirSync(imageFolderPath, images, rootPath, callback, count={fileCount:0, finishCount:0}){
|
||||||
|
var files = fs.readdirSync(imageFolderPath);
|
||||||
|
files.forEach(item => {
|
||||||
|
var fileInfo = fs.statSync(`${imageFolderPath}/${item}`)
|
||||||
|
if(fileInfo.isDirectory()){
|
||||||
|
// 该文件是一个目录, 则遍历该目录内容
|
||||||
|
readDirSync(`${imageFolderPath}/${item}`, images, rootPath, callback, count)
|
||||||
|
} else {
|
||||||
|
count.fileCount ++
|
||||||
|
var stream = fs.createReadStream(`${imageFolderPath}/${item}`)
|
||||||
|
var fsHash = crypto.createHash('md5')
|
||||||
|
|
||||||
|
stream.on('data', data => {
|
||||||
|
fsHash.update(data)
|
||||||
|
})
|
||||||
|
stream.on('end', () => {
|
||||||
|
count.finishCount ++
|
||||||
|
images.push({
|
||||||
|
name: `${imageFolderPath}/${item}`.replace(rootPath, ''),
|
||||||
|
md5: fsHash.digest('hex')
|
||||||
|
})
|
||||||
|
if(count.fileCount === count.finishCount && typeof callback === 'function') {
|
||||||
|
callback(images.sort(sortName))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function (rootPath, imageFloder, callback) {
|
||||||
|
readDirSync(path.resolve(rootPath, imageFloder), [], rootPath, callback)
|
||||||
|
}
|
||||||
48
gulpfile.js
@ -1,12 +1,14 @@
|
|||||||
const gulp = require('gulp'),
|
const gulp = require('gulp'),
|
||||||
htmlmin = require('gulp-htmlmin'), // html压缩组件
|
htmlmin = require('gulp-htmlmin'), //html压缩组件
|
||||||
htmlclean = require('gulp-htmlclean'), // html清理组件
|
htmlclean = require('gulp-htmlclean'), //html清理组件
|
||||||
plumber = require('gulp-plumber'), // 容错组件(发生错误不跳出任务,并报出错误内容)
|
plumber = require('gulp-plumber'), //容错组件(发生错误不跳出任务,并报出错误内容)
|
||||||
Hexo = require('hexo'),
|
Hexo = require('hexo')
|
||||||
log = require('fancy-log') // gulp的日志输出
|
|
||||||
|
|
||||||
// 程序执行的传参
|
// 程序执行的传参
|
||||||
const argv = require('optimist')
|
const argv = require('optimist')
|
||||||
|
// .demand(['accessKey', 'accessSecret', 'deployPath'])
|
||||||
|
.describe('accessKey', '网易云对象存储key')
|
||||||
|
.describe('accessSecret', '网易云对象存储secret')
|
||||||
.describe('deployPath', '静态化后发布的目录')
|
.describe('deployPath', '静态化后发布的目录')
|
||||||
.argv
|
.argv
|
||||||
|
|
||||||
@ -31,8 +33,7 @@ gulp.task('compressHtml', () => {
|
|||||||
unprotect: /<script [^>]*\btype="text\/x-handlebars-template"[\s\S]+?<\/script>/ig //特殊处理
|
unprotect: /<script [^>]*\btype="text\/x-handlebars-template"[\s\S]+?<\/script>/ig //特殊处理
|
||||||
}
|
}
|
||||||
const minOption = {
|
const minOption = {
|
||||||
collapseWhitespace: true, //删除html中的空白
|
collapseWhitespace: true, //压缩HTML
|
||||||
conservativeCollapse: false, //将多个空白折叠为1空白(永远不要完全移除), 必须与 collapseWhitespace=true 一起使用
|
|
||||||
collapseBooleanAttributes: true, //省略布尔属性的值 <input checked="true"/> ==> <input />
|
collapseBooleanAttributes: true, //省略布尔属性的值 <input checked="true"/> ==> <input />
|
||||||
removeEmptyAttributes: true, //删除所有空属性值 <input id="" /> ==> <input />
|
removeEmptyAttributes: true, //删除所有空属性值 <input id="" /> ==> <input />
|
||||||
removeScriptTypeAttributes: true, //删除<script>的type="text/javascript"
|
removeScriptTypeAttributes: true, //删除<script>的type="text/javascript"
|
||||||
@ -40,7 +41,7 @@ gulp.task('compressHtml', () => {
|
|||||||
removeComments: true, //清除HTML注释
|
removeComments: true, //清除HTML注释
|
||||||
minifyJS: true, //压缩页面JS
|
minifyJS: true, //压缩页面JS
|
||||||
minifyCSS: true, //压缩页面CSS
|
minifyCSS: true, //压缩页面CSS
|
||||||
minifyURLs: false //替换页面URL
|
minifyURLs: true //替换页面URL
|
||||||
}
|
}
|
||||||
return gulp.src('./public/**/*.html')
|
return gulp.src('./public/**/*.html')
|
||||||
.pipe(plumber())
|
.pipe(plumber())
|
||||||
@ -49,22 +50,37 @@ gulp.task('compressHtml', () => {
|
|||||||
.pipe(gulp.dest('./public'))
|
.pipe(gulp.dest('./public'))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 拷贝图片
|
// 同步图片到对象存储仓库
|
||||||
gulp.task('copyImage', () => {
|
gulp.task('syncImages', () => {
|
||||||
const deploy = require('./deploy_utils/deploy')
|
const listImages = require('./deploy_utils/list_images')
|
||||||
return deploy.exec('./images', './public/images')
|
if(!argv.accessKey || !argv.accessSecret) {
|
||||||
|
return Promise.resolve('未获得accessKey以及accessSecret, 跳过图片同步').then(console.log)
|
||||||
|
}
|
||||||
|
// 同步当前本地存在的所有图片
|
||||||
|
return new Promise(resolve => {
|
||||||
|
listImages(`${process.cwd()}/source/`, 'images/', resolve)
|
||||||
|
}).then(imagesList => {
|
||||||
|
const ImageSynchronizer = require('./deploy_utils/image_synchronize')
|
||||||
|
const nosSetting = {
|
||||||
|
defaultBucket: 'blog-cdn',
|
||||||
|
endpoint: 'http://nos-eastchina1.126.net',
|
||||||
|
accessKey: argv.accessKey,
|
||||||
|
accessSecret: argv.accessSecret
|
||||||
|
}
|
||||||
|
const imageSynchronizer = new ImageSynchronizer(nosSetting, imagesList, `${process.cwd()}/source/`)
|
||||||
|
return imageSynchronizer.synchronize('images/')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 发布
|
|
||||||
gulp.task('deploy', () => {
|
gulp.task('deploy', () => {
|
||||||
if(!argv.deployPath) {
|
if(!argv.deployPath) {
|
||||||
return Promise.resolve('未获得deployPath, 跳过发布').then(log)
|
return Promise.resolve('未获得deployPath, 跳过发布').then(console.log)
|
||||||
}
|
}
|
||||||
const deploy = require('./deploy_utils/deploy')
|
const deploy = require('./deploy_utils/deploy')
|
||||||
return deploy.exec('./public', argv.deployPath, true)
|
return deploy.exec('./public', argv.deployPath, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 默认任务
|
// 默认任务
|
||||||
gulp.task('default',
|
gulp.task('default',
|
||||||
gulp.series('generate', 'compressHtml', 'copyImage', 'deploy') // 串行执行任务
|
gulp.series('generate', 'compressHtml', 'syncImages', 'deploy') // 串行执行任务
|
||||||
)
|
)
|
||||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
30
package.json
@ -1,35 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "blog-web",
|
"name": "hexo_blog",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"hexo": {
|
"hexo": {
|
||||||
"version": "6.0.0"
|
"version": "3.8.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "hexo server",
|
"dev": "hexo server",
|
||||||
"build": "gulp"
|
"build": "gulp"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hexo": "^6.0.0",
|
"hexo": "^3.8.0",
|
||||||
"hexo-generator-archive": "^1.0.0",
|
"hexo-generator-archive": "^0.1.5",
|
||||||
"hexo-generator-baidu-sitemap": "^0.1.9",
|
"hexo-generator-baidu-sitemap": "^0.1.6",
|
||||||
"hexo-generator-category": "^1.0.0",
|
"hexo-generator-category": "^0.1.3",
|
||||||
"hexo-generator-feed": "^3.0.0",
|
"hexo-generator-feed": "^1.2.2",
|
||||||
"hexo-generator-index": "^2.0.0",
|
"hexo-generator-index": "^0.2.1",
|
||||||
"hexo-generator-json-content": "^4.2.3",
|
"hexo-generator-json-content": "^4.1.3",
|
||||||
"hexo-generator-sitemap": "^2.2.0",
|
"hexo-generator-sitemap": "^1.2.0",
|
||||||
"hexo-generator-tag": "^1.0.0",
|
"hexo-generator-tag": "^0.2.0",
|
||||||
"hexo-renderer-ejs": "^2.0.0",
|
"hexo-renderer-ejs": "^0.3.1",
|
||||||
"hexo-renderer-marked": "^5.0.0",
|
"hexo-renderer-marked": "^0.3.2",
|
||||||
"hexo-server": "^3.0.0",
|
"hexo-server": "^0.3.3",
|
||||||
"hexo-wordcount": "^6.0.1"
|
"hexo-wordcount": "^6.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@xgheaven/nos-node-sdk": "^0.2.5",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"gulp-htmlclean": "^2.7.22",
|
"gulp-htmlclean": "^2.7.22",
|
||||||
"gulp-htmlmin": "^5.0.1",
|
"gulp-htmlmin": "^5.0.1",
|
||||||
"gulp-plumber": "^1.2.1",
|
"gulp-plumber": "^1.2.1",
|
||||||
"nunjucks": "^3.2.3",
|
|
||||||
"optimist": "^0.6.1"
|
"optimist": "^0.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
scaffolds/draft.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: {{ title }}
|
||||||
|
tags:
|
||||||
|
---
|
||||||
4
scaffolds/page.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: {{ title }}
|
||||||
|
date: {{ date }}
|
||||||
|
---
|
||||||
5
scaffolds/post.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: {{ title }}
|
||||||
|
date: {{ date }}
|
||||||
|
tags:
|
||||||
|
---
|
||||||
9
scripts/filter.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
hexo.extend.filter.register('before_post_render', function(data){
|
||||||
|
// data.raw 是原始的文件内容
|
||||||
|
// data.content 是处理过代码块语法高亮的内容
|
||||||
|
if(hexo.config.picture_cdn) {
|
||||||
|
data.content = data.content.replace(/\]\s*\((?=(?!http).*?\))/gi,
|
||||||
|
`](${hexo.config.picture_cdn}`)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
@ -1,34 +0,0 @@
|
|||||||
const nunjucks = require('nunjucks')
|
|
||||||
const path = require('path')
|
|
||||||
const fs = require('fs')
|
|
||||||
|
|
||||||
const env = new nunjucks.configure({ autoescape: false })
|
|
||||||
env.addFilter('noControlChars', function(str) {
|
|
||||||
return str && str.replace(/[\x00-\x1F\x7F]/g, '')
|
|
||||||
})
|
|
||||||
|
|
||||||
const searchTmplSrc = path.join(__dirname, '../templates/articles.xml')
|
|
||||||
|
|
||||||
hexo.extend.generator.register('xml', function(locals){
|
|
||||||
const searchTmpl = nunjucks.compile(fs.readFileSync(searchTmplSrc, 'utf8'), env)
|
|
||||||
const descCompare = function(value1, value2) {
|
|
||||||
if(value1 > value2) {
|
|
||||||
return -1
|
|
||||||
} else if(value1 < value2) {
|
|
||||||
return 1
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const posts = locals.posts.toArray().sort(function(item1, item2){
|
|
||||||
return descCompare(item1.updateDate || item1.date, item2.updateDate || item2.date)
|
|
||||||
}).slice(0, 10)
|
|
||||||
const xmlData = searchTmpl.render({
|
|
||||||
posts,
|
|
||||||
root: this.config.root
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
path: 'articles.xml',
|
|
||||||
data: xmlData
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -1,212 +0,0 @@
|
|||||||
---
|
|
||||||
title: Jest单元测试
|
|
||||||
date: 2019-07-21 00:41:10
|
|
||||||
tags:
|
|
||||||
- JavaScript
|
|
||||||
- 单元测试
|
|
||||||
categories:
|
|
||||||
- JavaScript
|
|
||||||
---
|
|
||||||
|
|
||||||
`Jest`是由Facebook开源的一个测试框架,它集成了断言库、mock、快照测试、覆盖率报告等功能。它非常适合用来测试React代码,但不仅仅如此,所有的js代码都可以使用Jest进行单元测试
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
### 安装
|
|
||||||
直接执行`yarn add jest --dev`或者`npm install jest --save-dev`来安装到nodejs项目
|
|
||||||
会附带同时安装jest-cli这个命令行工具
|
|
||||||
可以在命令行直接执行jest命令运行单元测试代码
|
|
||||||
|
|
||||||
所以可以在package.json当中添加
|
|
||||||
```json
|
|
||||||
"scripts": {
|
|
||||||
"test": "jest"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
运行测试的时候直接执行`yarn test`或者`npm run test`即可
|
|
||||||
当然也可以用npx, 也就是`npx jest`
|
|
||||||
|
|
||||||
### Hello World
|
|
||||||
如果我们编写了一个函数, 要测试它的执行是否能达到预期结果
|
|
||||||
```javascript
|
|
||||||
// main.js
|
|
||||||
function sum(a, b) {
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
module.exports = { sum }
|
|
||||||
```
|
|
||||||
jest会递归查找项目当中所有的名为`*.test.js`以及`*.spec.js`
|
|
||||||
通常是把单元测试文件与源码文件同名(不是必须)
|
|
||||||
```javascript
|
|
||||||
// main.test.js
|
|
||||||
const { sum } = require('../main')
|
|
||||||
|
|
||||||
test('Adding 2 + 3 equals 5', () => {
|
|
||||||
expect(sum(2, 3)).toBe(5)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
> 这里虽然使用了jest提供的一些函数,但是测试代码当中并不需要进行引入
|
|
||||||
jest会帮我们引入这些
|
|
||||||
test也可以用it,完全一样,没有差别
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
test是运行一个测试用例,第一个参数是对该测试的描述,会在执行结果中打印出来
|
|
||||||
在此代码中,`expect(sum(2, 3))`返回一个“期望”对象,`.toBe(2)`是匹配器。匹配器将期望的结果(实际值)与自己的参数(期望值)进行比较
|
|
||||||
当Jest运行时,它会跟踪所有失败的匹配器,并打印出错误信息
|
|
||||||
|
|
||||||
常用的匹配器
|
|
||||||
+ `toBe` 使用 Object.is 判断是否严格相等。
|
|
||||||
+ `toEqual` 递归检查对象或数组的每个字段。
|
|
||||||
+ `toBeNull` 只匹配 null。
|
|
||||||
+ `toBeUndefined` 只匹配 undefined。
|
|
||||||
+ `toBeDefined` 只匹配非 undefined。
|
|
||||||
+ `toBeTruthy` 只匹配真。
|
|
||||||
+ `toBeFalsy` 只匹配假。
|
|
||||||
+ `toBeGreaterThan` 实际值大于期望。
|
|
||||||
+ `toBeGreaterThanOrEqual` 实际值大于或等于期望值
|
|
||||||
+ `toBeLessThan` 实际值小于期望值。
|
|
||||||
+ `toBeLessThanOrEqual` 实际值小于或等于期望值。
|
|
||||||
+ `toBeCloseTo` 比较浮点数的值,避免误差。
|
|
||||||
+ `toMatch` 正则匹配。
|
|
||||||
+ `toContain` 判断数组中是否包含指定项。
|
|
||||||
+ `toHaveProperty(keyPath, value)` 判断对象中是否包含指定属性。
|
|
||||||
+ `toThrow` 判断是否抛出指定的异常。
|
|
||||||
+ `toBeInstanceOf` 判断对象是否是某个类的实例,底层使用 instanceof。
|
|
||||||
|
|
||||||
所有的匹配器均可以使用`.not`取反
|
|
||||||
比如
|
|
||||||
```javascript
|
|
||||||
test('Adding 2 + 3 not equals 10', () => {
|
|
||||||
expect(sum(2, 3)).not.toBe(10)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Promise对象
|
|
||||||
当然实际情况一般没有这么简单,很多需要异步操作的函数返回的都是Promise对象
|
|
||||||
```javascript
|
|
||||||
const func1 = async () => {
|
|
||||||
return Promise.resolve('success')
|
|
||||||
}
|
|
||||||
const func2 = async () => {
|
|
||||||
return Promise.reject('failed')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
测试代码
|
|
||||||
```javascript
|
|
||||||
test('the func1 should resolve success', () => {
|
|
||||||
return expect(func1()).resolves.toBe('success')
|
|
||||||
})
|
|
||||||
test('the func1 should reject failed', () => {
|
|
||||||
return expect(func2()).rejects.toBe('failed')
|
|
||||||
})
|
|
||||||
```
|
|
||||||
也可以使用await
|
|
||||||
```javascript
|
|
||||||
test('the func1 should resolve success', async () => {
|
|
||||||
const result = await func1()
|
|
||||||
expect(result).toBe('success')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('the func2 should reject failed', async () => {
|
|
||||||
try {
|
|
||||||
await func2()
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).toBe('failed')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 抛出错误的匹配
|
|
||||||
可以使用`toThrow`或者`toThrowError`(并没发现这两者的不同)来校验函数抛出了指定的错误
|
|
||||||
```javascript
|
|
||||||
const func3 = () => {
|
|
||||||
throw new Error('this error')
|
|
||||||
}
|
|
||||||
|
|
||||||
test('the func3 throw Error', () =>{
|
|
||||||
function func3Wrapper() {
|
|
||||||
func3()
|
|
||||||
}
|
|
||||||
expect(func3Wrapper).toThrowError()
|
|
||||||
expect(func3Wrapper).toThrowError('this error')
|
|
||||||
expect(func3Wrapper).toThrowError(/^this/)
|
|
||||||
expect(func3Wrapper).toThrowError(new Error('this error'));
|
|
||||||
})
|
|
||||||
```
|
|
||||||
需要注意的是,抛出异常的方法**必须放在包装函数**内
|
|
||||||
也就是给expect传递一个函数,而不是目标函数的执行结果
|
|
||||||
否则无法捕获异常,也就无法判断抛出的异常是否匹配
|
|
||||||
参数是可选的,可以是
|
|
||||||
+ 字符串 - 与Error对象的message完全一致
|
|
||||||
+ 正则对象 - 与Error对象的message可以匹配
|
|
||||||
+ Error对象 - 与抛出的Error对象一致
|
|
||||||
|
|
||||||
### 钩子函数
|
|
||||||
Jest提供了四个测试用例的钩子:**beforeAll、afterAll、beforeEach、afterEach**。
|
|
||||||
`beforeAll` 和 `afterAll` 会在所有测试用例之前和所有测试用例之后执行一次。
|
|
||||||
`beforeEach` 和 `afterEach` 会在每个测试用例之前和之后执行。
|
|
||||||
|
|
||||||
如果测试用例较多,可以用`describe`将测试用例分组
|
|
||||||
在describe块中的钩子函数只作用于块内的测试用例:
|
|
||||||
```javascript
|
|
||||||
beforeAll(() => console.log('out - beforeAll')) // 1
|
|
||||||
afterAll(() => console.log('out - afterAll')) // 12
|
|
||||||
beforeEach(() => console.log('out - beforeEach')) // 2,6
|
|
||||||
afterEach(() => console.log('out - afterEach')) // 4,10
|
|
||||||
test('', () => console.log('out - test')) // 3
|
|
||||||
describe('Scoped / Nested block', () => {
|
|
||||||
beforeAll(() => console.log('in - beforeAll')) // 5
|
|
||||||
afterAll(() => console.log('in - afterAll')) // 11
|
|
||||||
beforeEach(() => console.log('in - beforeEach')) // 7
|
|
||||||
afterEach(() => console.log('in - afterEach')) // 9
|
|
||||||
test('', () => console.log('in - test')) // 8
|
|
||||||
})
|
|
||||||
```
|
|
||||||
外部的beforeAll会先于所有分组内的执行
|
|
||||||
外部的afterAll会在所有分组执行完毕后执行
|
|
||||||
|
|
||||||
外部的beforeEach会在所有测试用例执行前执行,并且先于分组内的beforeEach
|
|
||||||
外部的afterEach会在所有测试用例执行后执行,晚于分组内的beforeEach
|
|
||||||
|
|
||||||
这些钩子函数通常用于测试用例执行前后一些资源的初始化和销毁操作
|
|
||||||
|
|
||||||
### Mock函数
|
|
||||||
调用`jest.fn()`即可获得一个mock函数。 Mock函数有一个特殊的mock属性,保存着函数的调用信息
|
|
||||||
比如我们需要测试多组数据,并提供入参和返回值的期望
|
|
||||||
```javascript
|
|
||||||
function sum(a, b) {
|
|
||||||
return a + b
|
|
||||||
}
|
|
||||||
|
|
||||||
test('test forEach function', () => {
|
|
||||||
const sumCallback = jest.fn(sum)
|
|
||||||
const testData = [
|
|
||||||
[[1,2], 3],
|
|
||||||
[[2,3], 5],
|
|
||||||
[[3,5], 8],
|
|
||||||
[[101,102], 203]
|
|
||||||
]
|
|
||||||
testData.forEach(item => {
|
|
||||||
sumCallback.apply(null, item[0])
|
|
||||||
})
|
|
||||||
expect(sumCallback.mock.calls.length).toBe(testData.length)
|
|
||||||
testData.forEach((item,index) => {
|
|
||||||
// 通过calls属性可以拿到每次调用的入参(数组形式)
|
|
||||||
expect(sumCallback.mock.calls[index]).toEqual(item[0])
|
|
||||||
// 通过results属性可以拿到每次调用的返回值
|
|
||||||
expect(sumCallback.mock.results[index].value).toBe(item[1])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Jest配置
|
|
||||||
Jest可以在package.json当中通过`jest`属性来指定配置项
|
|
||||||
或者默认引入`jest.config.js`(如果存在)
|
|
||||||
也可以通过`--config`参数指定配置文件,该配置文件可以是json格式或者js格式
|
|
||||||
js格式需要使用`module.exports`来暴露出配置内容对象,供jest获取
|
|
||||||
|
|
||||||
执行`jest --init`可以初始化一个配置文件,包含多数配置项的默认值
|
|
||||||
|
|
||||||
其他常用参数
|
|
||||||
+ `--watch` 以监视模式启动
|
|
||||||
+ `--coverage` 生成覆盖率报告
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
---
|
|
||||||
title: Web-Workers
|
|
||||||
date: 2019-09-21 19:22:06
|
|
||||||
tags:
|
|
||||||
- JavaScript
|
|
||||||
- Web Worker
|
|
||||||
categories:
|
|
||||||
- JavaScript
|
|
||||||
---
|
|
||||||
|
|
||||||
在较早的时候,JavaScript本身是没有多线程的,如果执行耗时长的同步操作会造成页面假死影响用户体验
|
|
||||||
HTML5提供了`Web Workers API`,可以解决这个问题
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
并非所有耗时操作都可以用异步解决
|
|
||||||
比如密集的计算或者一些高延迟的任务
|
|
||||||
|
|
||||||
简单来说,Web Workers可以在一个子线程当中运行一个js脚本,这个js脚本当中可能有一些耗时的操作
|
|
||||||
从而保证主线程不会被阻塞
|
|
||||||
并且提供了一些机制在主子线程之间进行通信
|
|
||||||
|
|
||||||
### 基本用法
|
|
||||||
```javascript
|
|
||||||
var wk = new Worker('./worker.js', {})
|
|
||||||
```
|
|
||||||
> 第一个参数是worker将执行的脚本的URL,它必须遵守同源策略。
|
|
||||||
第二个参数是配置项(可选),有以下三个有效项
|
|
||||||
+ type:指定要创建的工作进程类型的名称。如果未指定,则默认使用classic。
|
|
||||||
+ credentials:指定要用于工作进程的凭据类型的名称。如果未指定,或者类型为classic,则使用的默认值为省略(不需要凭据)。
|
|
||||||
+ name:一个字符串,指定表示工作进程作用域的专用WorkerGlobalScope的标识名称,主要用于调试。
|
|
||||||
|
|
||||||
|
|
||||||
### 线程通信
|
|
||||||
不同的线程之间的通信是采用事件机制触发的,使用`message`事件进行通信
|
|
||||||
```javascript
|
|
||||||
// main.js
|
|
||||||
console.log('这里是主线程')
|
|
||||||
var wk = new Worker('./js/worker.js', {})
|
|
||||||
wk.onmessage = event => {
|
|
||||||
// 这里用来接收子线程发过来的消息
|
|
||||||
console.log(event.data)
|
|
||||||
}
|
|
||||||
wk.onerror = err => {
|
|
||||||
// 子线程抛出的错误
|
|
||||||
wk.terminate() // 立即终止worker
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
let btn = document.getElementById('send-msg')
|
|
||||||
btn.addEventListener('click', function(){
|
|
||||||
// 向子线程发送消息
|
|
||||||
wk.postMessage({msg: 'HOHO~'}) // 可以是任意的对象
|
|
||||||
})
|
|
||||||
console.log('这里还是主线程')
|
|
||||||
```
|
|
||||||
子线程对应的js代码
|
|
||||||
```javascript
|
|
||||||
// worker.js
|
|
||||||
console.log('这里是子线程')
|
|
||||||
let i = 1
|
|
||||||
function simpleCount() {
|
|
||||||
self.postMessage(++i)
|
|
||||||
if(i>100) {
|
|
||||||
self.close() // 子线程关闭自己
|
|
||||||
}
|
|
||||||
setTimeout(simpleCount, 1000)
|
|
||||||
}
|
|
||||||
simpleCount()
|
|
||||||
self.onmessage = event => {
|
|
||||||
console.log(event.data)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
+ 子线程中的`self`代表线程自身,如同非严格模式下主线程中this指向window
|
|
||||||
非严格模式下的子线程this指向这个self
|
|
||||||
+ 子线程无法读取主线程所在网页的DOM对象,也无法使用document、window、parent这些对象。
|
|
||||||
但是,子线程可以`navigator`对象和`location(只读)`对象
|
|
||||||
+ postMessage的对象传递是`值拷贝`,而且是深拷贝,并非地址拷贝
|
|
||||||
(实际实现的方式是先进行序列化,再进行反序列化)
|
|
||||||
+ 子线程中有可以加载脚本的函数 **importScripts('script1.js', 'script2.js')**
|
|
||||||
+ 子线程中不能执行诸如**alert confirm**等阻塞性的函数
|
|
||||||
+ 子线程执行**close**或者主线程对worker执行**terminate**会结束子线程
|
|
||||||
Worker对象如果没有引用指向,则会被JavaScript的垃圾回收器回收
|
|
||||||
其他情况下子线程即使空闲也不会停止,随时可以响应主线程的通信
|
|
||||||
+ `wk.onmessage`同样也可以使用`wk.addEventListener('message', function(){...})`
|
|
||||||
|
|
||||||
### 数据转移
|
|
||||||
主线程与子线程之间可以交换二进制数据,比如ArrayBuffer对象
|
|
||||||
但是拷贝方式发送数据会造成严重的性能问题
|
|
||||||
因此postMessage可以接受第二个参数,用来把二进制数据直接转移给子线程
|
|
||||||
(一旦转移,主线程将无法再使用这些数据)
|
|
||||||
```javascript
|
|
||||||
let obj = {
|
|
||||||
ab: new ArrayBuffer(10)
|
|
||||||
}
|
|
||||||
console.log('转交前长度', obj.ab.byteLength) // 10
|
|
||||||
wk.postMessage(obj,[obj.ab])
|
|
||||||
console.log('转交后长度', obj.ab.byteLength) // 0
|
|
||||||
```
|
|
||||||
在子线程当中同样通过event.data拿到该对象
|
|
||||||
```javascript
|
|
||||||
self.onmessage = function(event) {
|
|
||||||
console.log(event.data.ab)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
> 可以这样转移的对象须实现`Transferable`接口,这只是一个标记接口
|
|
||||||
实现该接口的类型有`ArrayBuffer`、`MessagePort`、`ImageBitmap`
|
|
||||||
|
|
||||||
### 可能的错误
|
|
||||||
+ 如果文档不允许启动worker,则会引发`SecurityError`
|
|
||||||
+ 如果脚本之一的MIME类型为 text/csv, image/\*, video/\*,或 audio/\*, 则会引发`NetworkError`。它应该始终是 text/javascript。
|
|
||||||
+ 如果url无法解析,则引发`SyntaxError`。
|
|
||||||
|
|
||||||
### 在webpack中使用
|
|
||||||
需要引入`work-loader`这个加载器
|
|
||||||
```javascript
|
|
||||||
let MyWorker = require('worker-loader!./test.js')
|
|
||||||
// 引入的这个对象可以直接当做Worker对象来用
|
|
||||||
let wk = new MyWorker()
|
|
||||||
wk.postMessage('test')
|
|
||||||
```
|
|
||||||
或者在webpack的配置文件当中添加规则
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
// 匹配 *.worker.js
|
|
||||||
test: /\.worker\.js$/,
|
|
||||||
use: {
|
|
||||||
loader: 'worker-loader',
|
|
||||||
options: {
|
|
||||||
name: '[name]:[hash:8].js',
|
|
||||||
// inline: true,
|
|
||||||
// fallback: false
|
|
||||||
// publicPath: '/scripts/workers/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 同源策略
|
|
||||||
Web Worker严格遵守同源策略,如果webpack的静态资源与应用代码不是同源的,那么很有可能就被浏览器给墙掉了,而且这种场景也经常发生。
|
|
||||||
对于Web Worker遇到这种情况,有两种解决方案
|
|
||||||
1. 通过设置worker-loader的选项参数`inline`把worker内联成blob数据格式,而不再是通过下载脚本文件的方式来使用worker
|
|
||||||
2. 通过设置worker-loader的选项参数`publicPath`来重写掉worker脚本的下载url,当然脚本也要存放到同样的位置
|
|
||||||
@ -42,98 +42,3 @@ reader.readAsDataURL(blob)
|
|||||||
```
|
```
|
||||||
File对象的原型是Blob对象, 所以本质上与来自input元素没什么区别
|
File对象的原型是Blob对象, 所以本质上与来自input元素没什么区别
|
||||||

|

|
||||||
|
|
||||||
### Blob对象
|
|
||||||
Blob对象使用同名构造函数创建
|
|
||||||
该构造函数接收两个参数, 第一个参数必须是 Array 类型的
|
|
||||||
第二个参数可选, 可以传入一个对象
|
|
||||||
有2个选项, 分别是`type`和`endings`
|
|
||||||
|
|
||||||
type 指定第一个参数的 MIME-Type, endings 指定第一个参数的数据格式,其可选值有 transparent(不变,默认) 和 native(随系统转换)
|
|
||||||
|
|
||||||
Blob对象上的属性和方法
|
|
||||||
+ `size`属性 - 对象中包含数据的大小(字节)
|
|
||||||
+ `type`属性 - 获取对象所包含数据的MIME类型
|
|
||||||
+ `slice(start, end, type)`方法 - 截取Blob对象中的一部分, 返回一个新的Blob对象, 包含开始索引, 不包含结束索引
|
|
||||||
|
|
||||||
> 使用slice和size, 可以实现大文件分段上传
|
|
||||||
|
|
||||||
### ArrayBuffer
|
|
||||||
|
|
||||||
> ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 不能直接操作,而是要通过类型数组对象或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
|
|
||||||
|
|
||||||
FileReader 有个 `readAsArrayBuffer` 方法,如果的被读的文件是二进制数据, 用这个方法去读应该是最合适的, 读出来的数据, 就是一个 Arraybuffer 对象
|
|
||||||
|
|
||||||
同名构造函数用于创建一个指定长度的, 内容全部为0的ArrayBuffer对象
|
|
||||||
```javascript
|
|
||||||
// 创建一个8字节的ArrayBuffer
|
|
||||||
let arrayBuffer = new ArrayBuffer(8)
|
|
||||||
```
|
|
||||||

|
|
||||||
> 这里的`[[Int8Array]]`等只是为了方便开发者查看的, 实际上并不能直接进行操作
|
|
||||||
|
|
||||||
由于无法对ArrayBuffer直接操作, 所以我们需要借助其他对象来操作
|
|
||||||
也就是`TypedArray`(类型数组对象)和`DataView`
|
|
||||||
|
|
||||||
### TypedArray
|
|
||||||
没有名为 TypedArray 的全局对象, 也没有一个名为的 TypedArray 构造函数, 它是一类对象的统称
|
|
||||||
有以下9个
|
|
||||||
<pre>
|
|
||||||
Int8Array();
|
|
||||||
Uint8Array();
|
|
||||||
Uint8ClampedArray();
|
|
||||||
Int16Array();
|
|
||||||
Uint16Array();
|
|
||||||
Int32Array();
|
|
||||||
Uint32Array();
|
|
||||||
Float32Array();
|
|
||||||
Float64Array();
|
|
||||||
</pre>
|
|
||||||
`Int`表示有符号整数, `Uint`表示无符号整数
|
|
||||||
|
|
||||||
**基本用法**
|
|
||||||
```javascript
|
|
||||||
let arrayBuffer = new ArrayBuffer(8);
|
|
||||||
console.log(arrayBuffer[0]); // undefined
|
|
||||||
|
|
||||||
let uint8Array = new Uint8Array(arrayBuffer);
|
|
||||||
console.log(uint8Array); // [0, 0, 0, 0, 0, 0, 0, 0]
|
|
||||||
|
|
||||||
uint8Array[0] = 1;
|
|
||||||
console.log(uint8Array[0]); // 1
|
|
||||||
console.log(uint8Array); // [1, 0, 0, 0, 0, 0, 0, 0]
|
|
||||||
```
|
|
||||||
|
|
||||||
### DataView
|
|
||||||
`DataView`功能跟TypedArray类似, 但它是一个**真实存在**的对象
|
|
||||||
提供各种方法来操作不同类型的数据
|
|
||||||
|
|
||||||
语法
|
|
||||||
<pre>
|
|
||||||
new DataView(buffer [, byteOffset [, byteLength]])
|
|
||||||
</pre>
|
|
||||||
**基本用法**
|
|
||||||
```javascript
|
|
||||||
let arrayBuffer = new ArrayBuffer(8);
|
|
||||||
let dataView = new DataView(arrayBuffer);
|
|
||||||
|
|
||||||
console.log(dataView.getUint8(1)); // 0
|
|
||||||
// 第一个参数是偏移量, 第二个参数是要设置为的整数
|
|
||||||
dataView.setUint8(1, 2);
|
|
||||||
console.log(dataView.getUint8(1)); // 2
|
|
||||||
console.log(dataView.getUint16(1)); // 512
|
|
||||||
|
|
||||||
dataView.setUint16(1, 255);
|
|
||||||
console.log(dataView.getUint16(1)); // 255
|
|
||||||
console.log(dataView.getUint8(1)); // 0
|
|
||||||
```
|
|
||||||
get方法用于读取, set方法用于写入
|
|
||||||
|
|
||||||
### btoa 和 atob
|
|
||||||
这两个方法可以实现字符串与base64编码的互转
|
|
||||||
|
|
||||||
+ `btoa` - 字符串转化为base64
|
|
||||||
+ `atob` - base64转化为字符串
|
|
||||||
|
|
||||||

|
|
||||||
中文会有报错, 需要先执行一下转码, 当然从base64转化为字符串也要进行解码
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
---
|
|
||||||
title: 优雅写异步与循环
|
|
||||||
date: 2019-06-29 23:39:46
|
|
||||||
tags:
|
|
||||||
- JavaScript
|
|
||||||
categories:
|
|
||||||
- JavaScript
|
|
||||||
---
|
|
||||||
|
|
||||||
JS当中的循环都是非异步的
|
|
||||||
包括但不限于
|
|
||||||
1. Array.prototype中的`forEach`
|
|
||||||
2. `for ... in` 语法
|
|
||||||
3. `for ... of` 语法
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
所以如果要在循环内的异步全部完成后做某些事情
|
|
||||||
例如
|
|
||||||
```javascript
|
|
||||||
// 这里我只是简单构造了一个异步
|
|
||||||
// 实际运用当中譬如数据库查询、文件读写等操作, 通常都是异步的
|
|
||||||
function show(num) {
|
|
||||||
Promise.resolve(num).then(console.log)
|
|
||||||
}
|
|
||||||
const arr = [100, 200, 300]
|
|
||||||
|
|
||||||
console.log('start')
|
|
||||||
arr.forEach(show)
|
|
||||||
console.log('end')
|
|
||||||
```
|
|
||||||
上面的写法根据事件队列的机制, 显然会先输出end, 再输出数组元素的值
|
|
||||||
当然我们可以在Promise的resolve函数当中判断是否到达了数组最后一个元素, 把输出end的操作写进resolve函数里面
|
|
||||||
显然这不够优雅, 而且很多时候也不方便这样做
|
|
||||||
|
|
||||||
实现方式
|
|
||||||
#### Promise.all
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const arr = [100, 200, 300]
|
|
||||||
|
|
||||||
console.log('start')
|
|
||||||
console.time('promise all in')
|
|
||||||
Promise.all(arr.map(show)).then(() => {
|
|
||||||
console.timeEnd('promise all in')
|
|
||||||
console.log('end')
|
|
||||||
})
|
|
||||||
```
|
|
||||||
为了比较执行性能的差异, 加了一个计时 ( Nodejs环境运行 )
|
|
||||||

|
|
||||||
|
|
||||||
#### async/await
|
|
||||||
```javascript
|
|
||||||
const arr = [100, 200, 300];
|
|
||||||
|
|
||||||
(async function() {
|
|
||||||
console.log('start')
|
|
||||||
console.time('await all in')
|
|
||||||
for await (let i of arr.map(show)) {}
|
|
||||||
console.timeEnd('await all in')
|
|
||||||
console.log('end')
|
|
||||||
})()
|
|
||||||
```
|
|
||||||

|
|
||||||
由于await必须用在async修饰的函数当中, 所以包装了一层
|
|
||||||
实际执行时间与Promise.all差不多
|
|
||||||
|
|
||||||
### One by one
|
|
||||||
这种方式效率最低,有点类似于同步语言中的循环,一个接着一个执行,耗时自然也就是所有异步方法耗时的总和。对资源的消耗最小。
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const arr = [100, 200, 300];
|
|
||||||
|
|
||||||
(async function() {
|
|
||||||
console.log('start')
|
|
||||||
console.time('await one by one')
|
|
||||||
for (let item of arr) {
|
|
||||||
await show(item)
|
|
||||||
}
|
|
||||||
console.timeEnd('await one by one')
|
|
||||||
console.log('end')
|
|
||||||
})()
|
|
||||||
```
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
---
|
|
||||||
title: 函数式进阶-管道
|
|
||||||
date: 2023-01-19 22:09:39
|
|
||||||
tags:
|
|
||||||
- JavaScript
|
|
||||||
- 函数
|
|
||||||
categories:
|
|
||||||
- JavaScript
|
|
||||||
---
|
|
||||||
|
|
||||||
管道处理是一种比较常见的api设计方式,意思是上一步的执行结果作为下一步的入参
|
|
||||||
顺序执行
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
如果我们需要在JS中实现这一点
|
|
||||||
当然可以这样做
|
|
||||||
```typescript
|
|
||||||
func1(func2(func3(arg)))
|
|
||||||
```
|
|
||||||
首先这样代码很难看,其次是这样嵌套的代码调用是固定的,没有做到动态的函数式编程
|
|
||||||
|
|
||||||
### 基本形式
|
|
||||||
|
|
||||||
如果要实现一个管道,可以这样做
|
|
||||||
```javascript
|
|
||||||
// pipe函数的参数接受若干个参数, 都是Function类型
|
|
||||||
const pipe = (...funcs) => {
|
|
||||||
// 这个pipe函数是会返回一个函数
|
|
||||||
return inputValue => { // 这个函数的初始入参作为第一个函数的调用入参, 之后将该函数返回值作为下一个函数的入参
|
|
||||||
return funcs.reduce((currentValue, currentFunction) => {
|
|
||||||
return currentFunction(currentValue)
|
|
||||||
}, inputValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
上面是使用`reduce`作用是正序执行,如果要逆序执行可以使用`reduceRight`
|
|
||||||
调用示例
|
|
||||||
```javascript
|
|
||||||
const func1 = value => {
|
|
||||||
return value * 10
|
|
||||||
}
|
|
||||||
const func2 = value => {
|
|
||||||
return value + 5
|
|
||||||
}
|
|
||||||
const func3 = value => {
|
|
||||||
return value * 0.5
|
|
||||||
}
|
|
||||||
const result = pipe(func1, func2, func3)(30)
|
|
||||||
console.log(result)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 异步函数
|
|
||||||
如果输入的函数是异步的,或者同步异步都有呢
|
|
||||||
那么函数的返回结果就不能立刻拿到了
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const pipeAsync = (...funcs) => {
|
|
||||||
return async (inputValue) => {
|
|
||||||
return funcs.reduce((chain, func) => {
|
|
||||||
return chain.then(func)
|
|
||||||
}, Promise.resolve(inputValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
区别就是不管函数是不是异步的,都当做异步处理,作为then的回调函数
|
|
||||||
调用示例
|
|
||||||
```javascript
|
|
||||||
const func4 = async value => {
|
|
||||||
return value * 10
|
|
||||||
}
|
|
||||||
|
|
||||||
const func5 = value => {
|
|
||||||
return value + 5
|
|
||||||
}
|
|
||||||
|
|
||||||
const func6 = async value => {
|
|
||||||
return value * 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeAsync(func4, func5, func6)(30).then(result2 => {
|
|
||||||
console.log(result2)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
---
|
|
||||||
title: 重新认识JSON.stringify
|
|
||||||
date: 2019-12-11 21:18:56
|
|
||||||
categories:
|
|
||||||
- JavaScript
|
|
||||||
tags:
|
|
||||||
- JavaScript
|
|
||||||
---
|
|
||||||
|
|
||||||
`JSON.stringify`方法用于将对象进行序列化
|
|
||||||
但是对于不同的对象,该方法有不同的表现,并且也可以巧妙利用它去优雅地达到一些目的
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
### 对于不同对象的处理
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const data = {
|
|
||||||
a: "aaa",
|
|
||||||
b: undefined,
|
|
||||||
c: Symbol("dd"),
|
|
||||||
fn: function() {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
arr: [1, undefined, Symbol("ee"), function(){}]
|
|
||||||
}
|
|
||||||
JSON.stringify(data) // {"a":"aaa","arr":[1,null,null,null]}
|
|
||||||
```
|
|
||||||
undefined、任意的函数以及symbol作为对象属性值时 JSON.stringify() 将**跳过**(忽略)对它们进行序列化
|
|
||||||
如果上述几种对象是数组当中的元素,将被序列化为 **null**
|
|
||||||
如果上述几种对象单独执行序列化,得到的结果都是 **undefined**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const data = {
|
|
||||||
a: NaN,
|
|
||||||
b: Infinity,
|
|
||||||
c: null
|
|
||||||
}
|
|
||||||
JSON.stringify(data) //{"a":null,"b":null,"c":null}
|
|
||||||
```
|
|
||||||
`NaN`、`Infinity`和`null`都会被序列化为null
|
|
||||||
对其单独执行也都是null
|
|
||||||
|
|
||||||
### toJSON
|
|
||||||
如果转换的对象当中包含`toJSON`函数
|
|
||||||
转换的结果将是该函数的返回值
|
|
||||||
```javascript
|
|
||||||
const data = {
|
|
||||||
a: "aaa",
|
|
||||||
toJSON() {
|
|
||||||
return "Hello"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JSON.stringify(data) // "Hello"
|
|
||||||
```
|
|
||||||
> 对于其中的每一个嵌套的对象,同样符合该特点
|
|
||||||
|
|
||||||
基于上述特性,对于Date对象,是可以正常序列化为UTC时间的字符串的
|
|
||||||
因为Date对象本身实现了该方法
|
|
||||||
|
|
||||||
### 深拷贝
|
|
||||||
如果可以不在意对特殊对象的处理,我们可以利用 JSON.stringify 来简单实现深拷贝
|
|
||||||
但是如果对象当中出现循环引用
|
|
||||||
就会抛出错误
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const data = {
|
|
||||||
a: 10
|
|
||||||
}
|
|
||||||
data.loopData = data
|
|
||||||
|
|
||||||
JSON.stringify(data)
|
|
||||||
/*
|
|
||||||
Uncaught TypeError: Converting circular structure to JSON
|
|
||||||
--> starting at object with constructor 'Object'
|
|
||||||
--- property 'loopData' closes the circle
|
|
||||||
at JSON.stringify (<anonymous>)
|
|
||||||
at <anonymous>:6:18
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 另外两个参数
|
|
||||||
JSON.stringify 还有另外两个可选参数`replacer`和`space`
|
|
||||||
|
|
||||||
replacer可以是一个函数,接收key和value两个参数,分别是对象中成员的键和值
|
|
||||||
该函数的返回值将被作为序列化的结果
|
|
||||||
|
|
||||||
所以我们可以利用这个函数,来改变上面提到的一些默认行为
|
|
||||||
```javascript
|
|
||||||
const data = {
|
|
||||||
a: '11',
|
|
||||||
b: undefined
|
|
||||||
}
|
|
||||||
JSON.stringify(data, (key, value)=>{
|
|
||||||
if(typeof value === 'undefined') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}) // {"a":"11","b":null}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
replacer参数也可以是一个数组
|
|
||||||
含义就是只有在这个数组当中包含的元素作为的key才会被序列化,其余会被忽略
|
|
||||||
```javascript
|
|
||||||
const data = {
|
|
||||||
a: '11',
|
|
||||||
b: '22'
|
|
||||||
}
|
|
||||||
JSON.stringify(data, ['b']) // {"b":"22"}
|
|
||||||
```
|
|
||||||
> 当然,按照开头提到的规则,该被忽略的还是会被忽略,哪怕它存在于这个数组当中
|
|
||||||
|
|
||||||
space这个参数用于美化输出
|
|
||||||
|
|
||||||
指定缩进用的空白字符串;如果参数是个数字,它代表有多少的空格;上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。
|
|
||||||
@ -45,4 +45,4 @@ C:\Program Files\MongoDB\Server\3.2\bin\mongo.exe
|
|||||||
|
|
||||||
7. **可视化工具**
|
7. **可视化工具**
|
||||||
|
|
||||||
可以使用robomongo作为可视化工具
|
可以试用robomongo作为可视化工具
|
||||||
@ -1,158 +0,0 @@
|
|||||||
---
|
|
||||||
title: 博客全文检索施工记录
|
|
||||||
date: 2019-7-9 10:15:00
|
|
||||||
tags:
|
|
||||||
- 全文检索
|
|
||||||
- MongoDB
|
|
||||||
categories:
|
|
||||||
- MongoDB
|
|
||||||
---
|
|
||||||
|
|
||||||
最近发现了一个提供全文检索服务的Algolia, 可以上传内容索引并且提供访问接口执行全文检索
|
|
||||||
可惜尝试之后发现上传内容索引时如果内容太长就会失败, 这就导致只能在摘要范围内搜索
|
|
||||||
因为已经有了后端API的支持, 所以考虑自己实现一下
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
### 分词
|
|
||||||
全文检索的关键步骤是分词, 中文分词做的比较好的就是jieba分词了
|
|
||||||
还提供了nodejs的包可以直接调用
|
|
||||||
当然底层仍是C++代码, 安装依赖时需要使用node-gyp编译
|
|
||||||
我在windows环境始终没能成功(已安装python和windows-build-tools), linux和mac环境倒是十分顺利
|
|
||||||
|
|
||||||
基本的调用方式
|
|
||||||
```typescript
|
|
||||||
import * as nodejieba from 'nodejieba'
|
|
||||||
nodejieba.load({}) // 加载词库, 只需调用一次
|
|
||||||
|
|
||||||
nodejieba.cut('这里是需要执行分词的文本', true)
|
|
||||||
```
|
|
||||||
对于文章内容和查询关键字都需要做分词操作
|
|
||||||
|
|
||||||
### 保存分词结果
|
|
||||||
在mongodb当中使用两个集合分别保存文章正文和分词结果
|
|
||||||
(因为mongodb限制每个文档的最大体积16MB, 这样做也是为了避免长文章导致的无法保存)
|
|
||||||
|
|
||||||
article集合
|
|
||||||

|
|
||||||
|
|
||||||
article_keys集合
|
|
||||||
这个集合用来保存分词的结果
|
|
||||||

|
|
||||||
|
|
||||||
### 执行检索
|
|
||||||
需要重点理解一下mongodb的`aggregate`, 这是一个管道操作, 接受一个数组
|
|
||||||
数组中的每个元素都是一步操作, 前一步操作的执行结果传递给下一步操作继续处理
|
|
||||||
|
|
||||||
检索需要实现以下几个目标
|
|
||||||
1. 自动分词: 比如"函数解决"这类常见中文词汇, 自动拆分为"函数","解决"
|
|
||||||
2. 强制分词: 比如"存储 过程"其中包含空白符分隔的, 强制拆分为"存储","过程"
|
|
||||||
3. 非完全匹配: 比如分词为"函数","解决", "函数"有匹配, "解决"无匹配, 也能查到"函数"匹配的
|
|
||||||
4. 相关度降序: 出现频率高的文章在前, 比如"函数","解决"在A文章共出现5次, B文章共出现3次, 则A文章在前
|
|
||||||
5. 摘要提取: 对于关键词所在正文内容提取, 关键词出现位置靠近则合并提取范围, 关键词过多则省略靠后的摘要
|
|
||||||
|
|
||||||
1和2可以利用jieba分词和正则做到, 查询过程主要实现3和4, 5在查询之后对正文处理做到
|
|
||||||
|
|
||||||
以下是目前设计的查询方案
|
|
||||||
主要目的是在一次查询当中做到计数和分页截取
|
|
||||||
```javascript
|
|
||||||
const splitedWords = ['函数', '过程'] //对查询关键词的分词处理结果
|
|
||||||
const page = { // 分页信息
|
|
||||||
start: 0,
|
|
||||||
limit: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
db.getCollection('article_keys').aggregate([
|
|
||||||
{$unwind: '$keys'}, // 把各个文档当中的keys字段打散统一执行查询
|
|
||||||
{$match: {keys: {$in: splitedWords}}}, // 数组的匹配筛选
|
|
||||||
{$group: {_id: '$article_id', num: {$sum: 1}}}, // 按照文章ID聚合, 并且计数(每个的数量就是关键词匹配到的次数)
|
|
||||||
{$sort: {num: -1}}, // 匹配次数降序(实现相关度降序)
|
|
||||||
{ $lookup: {
|
|
||||||
from: 'article',
|
|
||||||
localField: '_id',
|
|
||||||
foreignField: '_id',
|
|
||||||
as: 'articles',
|
|
||||||
},// 联查article, 目的是获取到对应的正文
|
|
||||||
},
|
|
||||||
{ $group: {_id: 1, articles: {$push: '$articles'}, total: {$sum: 1}}}, // 聚合获取数据总数
|
|
||||||
{ $project : { // 上一步输出的结果已经是一个文档, 其中包含的articles字段就是所有的匹配结果
|
|
||||||
_id: 1,
|
|
||||||
total: 1,
|
|
||||||
articles: { $slice: ['$articles', page.start, page.limit] }, // 分页截取
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
操作输出的结果就是包含分页数据和数据总数的一个对象了
|
|
||||||
需要注意的是, 倒数第二步执行聚合之后, 因为已经聚合为一个文档, 所以就不能再用`$skip`和`$limit`了
|
|
||||||
只能使用数组的操作函数`$slice`, 第一个参数为操作对象, 第二个参数是截取起点, 第三个参数是截取长度
|
|
||||||
|
|
||||||
### 摘要提取
|
|
||||||
这里是TypeScript语法, 使用原生JavaScript也一样
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* 创建文章搜索结果摘要, 使用<strong>标签高亮关键词
|
|
||||||
* @param content 文章正文内容
|
|
||||||
* @param keyWords 关键词们
|
|
||||||
* @param cutLen 每个关键词所在位置的截取区域长度
|
|
||||||
* @returns 文章摘要信息
|
|
||||||
*/
|
|
||||||
private createSummary(content: string, keyWords: string[], cutLen: number): string {
|
|
||||||
const cutRanges: number[][] = []
|
|
||||||
keyWords.forEach((keyWord: string) => {
|
|
||||||
const keyWordIndex: number = content.indexOf(keyWord)
|
|
||||||
if (keyWordIndex === -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const start: number = keyWordIndex - cutLen / 2 < 0 ? 0 : keyWordIndex - cutLen / 2
|
|
||||||
const end: number = keyWordIndex + cutLen / 2 + keyWord.length > content.length ? content.length : keyWordIndex + cutLen / 2 + keyWord.length
|
|
||||||
cutRanges.push([start, end])
|
|
||||||
})
|
|
||||||
cutRanges.sort((item1: number[], item2: number[]) => {
|
|
||||||
if (item1[0] > item2[0]) {
|
|
||||||
return 1
|
|
||||||
} else if (item1[0] < item2[0]) {
|
|
||||||
return -1
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
let summary = ''
|
|
||||||
let lastCutEnd = 0
|
|
||||||
for (let index = 0 ; index < cutRanges.length ; index++) {
|
|
||||||
const cutStart: number = cutRanges[index][0]
|
|
||||||
let cutEnd: number = cutRanges[index][1]
|
|
||||||
// 如果当前范围的末尾达到或超过下一个范围的开头
|
|
||||||
while (index < cutRanges.length - 1 && cutRanges[index][1] >= cutRanges[index + 1][0]) {
|
|
||||||
// 则把范围扩大到下一个范围的末尾
|
|
||||||
cutEnd = cutRanges[index + 1][1]
|
|
||||||
// 并将索引向前推进
|
|
||||||
index ++
|
|
||||||
}
|
|
||||||
if (summary.length) {
|
|
||||||
summary += ' ... '
|
|
||||||
}
|
|
||||||
summary += content.substring(cutStart, cutEnd)
|
|
||||||
lastCutEnd = cutEnd
|
|
||||||
if (summary.length > 150) { break }
|
|
||||||
}
|
|
||||||
summary = cutRanges[0][0] > 0 ? ('... ' + summary) : summary
|
|
||||||
summary += lastCutEnd < content.length ? ' ...' : ''
|
|
||||||
keyWords.forEach(keyWord => {
|
|
||||||
summary = summary.replace(new RegExp(CommonUtils.escapeRegexStr(keyWord), 'g'), `<strong>${keyWord}</strong>`)
|
|
||||||
})
|
|
||||||
return summary
|
|
||||||
}
|
|
||||||
```
|
|
||||||
主要思路就是找到每一个关键词所在位置, 确定截取区域, 注意几点
|
|
||||||
1. 截取区域到达全文开头或者末尾的处理
|
|
||||||
2. 截取范围按照升序排列以便截取后组合的结果正确
|
|
||||||
3. 两个相邻的关键词如果截取范围相连或者有重叠, 需要将两者的范围合并
|
|
||||||
4. 摘要总长度限制
|
|
||||||
|
|
||||||
### 性能表现
|
|
||||||
分词后的数据总量大约56万
|
|
||||||

|
|
||||||
|
|
||||||
添加索引之后基本每次查询的时间大约200ms, 基本还是不错的
|
|
||||||

|
|
||||||
@ -1,257 +0,0 @@
|
|||||||
---
|
|
||||||
title: Flutter初见
|
|
||||||
date: 2019-10-21 22:38:25
|
|
||||||
tags:
|
|
||||||
- flutter
|
|
||||||
- dart
|
|
||||||
categories:
|
|
||||||
- flutter
|
|
||||||
---
|
|
||||||
|
|
||||||
### 搭建开发环境
|
|
||||||
1. 下载解压Flutter的sdk,并且将其中的bin目录配置到环境变量
|
|
||||||
2. Android Studio安装安卓SDK,通常启动时如果未安装过都会自动执行下载安装
|
|
||||||
<!-- more -->
|
|
||||||
3. Android Studio安装Flutter的插件
|
|
||||||

|
|
||||||
安装flutter插件时会自动安装其依赖的dart插件
|
|
||||||
4. 在Android Studio的`AVD Manager`当中创建模拟器(需要选择屏幕尺寸以及安卓系统版本,当前系统中不存在的版本需要下载)
|
|
||||||
如果使用真机调试运行,该步骤可以跳过
|
|
||||||
|
|
||||||
|
|
||||||
执行`flutter doctor`可以检验当前环境当中存在的问题
|
|
||||||

|
|
||||||
可以看到IDEA也是支持的,但是我们不打算使用IDEA来进行开发,所以就不在IDEA当中安装插件了
|
|
||||||
No devices available代表还未启动虚拟机或者连接真机
|
|
||||||
其余几项都是校验通过的
|
|
||||||
|
|
||||||
### 创建flutter项目
|
|
||||||
当Android Studio的flutter插件安装无误之后,就可以创建flutter项目了
|
|
||||||

|
|
||||||
|
|
||||||
新建的是一个flutter的demo项目
|
|
||||||
包含一个标题栏,一个按钮,和中间区域内的若干文字
|
|
||||||
点击按钮时进行计数,文字会相应变化
|
|
||||||

|
|
||||||
|
|
||||||
项目当中包含一个main.dart文件
|
|
||||||
内容如下
|
|
||||||
```dart
|
|
||||||
// 引入material样式
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
// 程序运行入口
|
|
||||||
void main() => runApp(MyApp());
|
|
||||||
|
|
||||||
// App实体类
|
|
||||||
class MyApp extends StatelessWidget {
|
|
||||||
// This widget is the root of your application.
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'Flutter Demo',
|
|
||||||
theme: ThemeData(
|
|
||||||
// 主题样式
|
|
||||||
primarySwatch: Colors.blue,
|
|
||||||
),
|
|
||||||
// 首页对象以及首页的标题
|
|
||||||
home: MyHomePage(title: 'Flutter Demo Home Page'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
|
||||||
MyHomePage({Key key, this.title}) : super(key: key);
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_MyHomePageState createState() => _MyHomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
|
||||||
int _counter = 0;
|
|
||||||
|
|
||||||
void _incrementCounter() {
|
|
||||||
setState(() { // 计数方法
|
|
||||||
_counter++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
// 标题栏
|
|
||||||
title: Text(widget.title),
|
|
||||||
),
|
|
||||||
body: Center( // 居中元素
|
|
||||||
// 子元素
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
'You have pushed the button this many times:',
|
|
||||||
),
|
|
||||||
Text( // 文字元素以及样式
|
|
||||||
'$_counter',
|
|
||||||
style: Theme.of(context).textTheme.display1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 浮动按钮
|
|
||||||
floatingActionButton: FloatingActionButton(
|
|
||||||
onPressed: _incrementCounter, // 点击事件
|
|
||||||
tooltip: 'Increment',
|
|
||||||
child: Icon(Icons.add), // 子元素是一个图标
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
以上代码当中构建对象都省略了new关键字
|
|
||||||
其中的每个对象都是一个组件(Widget)
|
|
||||||
|
|
||||||
### 常用组件
|
|
||||||
组件以及组件的一些属性,基本算是前端html和css的思想
|
|
||||||
|
|
||||||
#### Text组件
|
|
||||||
这是一个文本组件,可以用于在App当中显示文字
|
|
||||||
```dart
|
|
||||||
Text(
|
|
||||||
'这是一段文字',
|
|
||||||
textAlign: TextAlign.left, // 文本对齐方式(左对齐)
|
|
||||||
maxLines: 3, // 最大显示行数
|
|
||||||
overflow: TextOverflow.ellipsis, // 文本长度溢出的处理方式(显示为...)
|
|
||||||
style: TextStyle(
|
|
||||||
color: Color.fromARGB(255, 20, 130, 40), // 文字颜色
|
|
||||||
fontSize: 20, // 字号大小
|
|
||||||
decoration: TextDecoration.underline, // 下划线
|
|
||||||
decorationStyle: TextDecorationStyle.solid // 下划线类型
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 容器组件
|
|
||||||
就是`Container`类以及它的一些子类
|
|
||||||
可以简单理解为div,通常用来方便控制布局
|
|
||||||
```dart
|
|
||||||
Container(
|
|
||||||
child: Text(
|
|
||||||
'这是一段文字',
|
|
||||||
style: TextStyle(fontSize: 40),
|
|
||||||
),
|
|
||||||
alignment: Alignment.bottomLeft, // 底部居左对齐
|
|
||||||
color: Color.fromARGB(255, 50, 50, 50), //内部文字的颜色
|
|
||||||
margin: EdgeInsets.fromLTRB(10, 20, 10, 30), // 左 上 右 下
|
|
||||||
)
|
|
||||||
```
|
|
||||||
属性也都很容易理解,可以设置容器的宽高,内外边距等等
|
|
||||||
|
|
||||||
**decoration**属性可以用来修饰容器
|
|
||||||
比如边框、背景色等
|
|
||||||
```dart
|
|
||||||
Container(
|
|
||||||
width: 500,
|
|
||||||
height: 300,
|
|
||||||
padding: EdgeInsets.all(20), // 内边距
|
|
||||||
decoration: BoxDecoration( // 容器修饰
|
|
||||||
// 边框
|
|
||||||
border: Border.fromBorderSide(BorderSide(color:Colors.amber, width:5)),
|
|
||||||
// 背景过渡色
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [Colors.blueAccent, Colors.pinkAccent]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
child: Text('Hello'),
|
|
||||||
),
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 图片组件
|
|
||||||
`Image`是图片组件
|
|
||||||
显示图片的方式有4种
|
|
||||||
+ **Image.asset** - 加载资源图片,资源图片需要打包在APP当中
|
|
||||||
+ **Image.network** - 网络资源图片
|
|
||||||
+ **Image.file** - 设备本地的图片,比如相机拍照后的图片预览
|
|
||||||
+ **Image.memory** - 加载内存中的图片,Uint8List
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Image.network(
|
|
||||||
'https://www.colorfulsweet.site/api/v1/common/randomBg?id=5d79b8606867833591833ae4', // 图片地址
|
|
||||||
scale: 1.5, // 缩放(值越大图片显示越小)
|
|
||||||
fit: BoxFit.fitHeight, // fit属性指定控制图片拉伸适应容器的方式, 这里是按高度适应
|
|
||||||
),
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 图片混合模式
|
|
||||||
可以给图片混合上一种颜色,类似于添加滤镜
|
|
||||||
```dart
|
|
||||||
Image.network(
|
|
||||||
'https://www.colorfulsweet.site/api/v1/common/randomBg?id=5d79b8606867833591833ae4',
|
|
||||||
color: Colors.lightGreen, // 混合的颜色
|
|
||||||
colorBlendMode: BlendMode.lighten, // 混合的模式
|
|
||||||
repeat: ImageRepeat.repeat, // 平铺充满容器
|
|
||||||
),
|
|
||||||
```
|
|
||||||
#### 列表组件
|
|
||||||
就是`ListView`组件
|
|
||||||
通常用于数据列表的展示
|
|
||||||
```dart
|
|
||||||
ListView(children: <Widget>[
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Icons.access_time),
|
|
||||||
title: Text('这是第一条'),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(Icons.android),
|
|
||||||
title: Text('这是第二条'),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
```
|
|
||||||
ListView其中的children是一个Widget数组
|
|
||||||
其中可以是任意的组件,纵向列表通常使用`ListTile`(列表瓦片)来作为列表成员
|
|
||||||
其中的leading和title都可以是任意组件,可以利用各种组件的组合来构造出漂亮的列表
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
ListView有属性`scrollDirection`,表示列表元素的排列方向,默认是纵向的
|
|
||||||
如果设置为 **Axis.horizontal** 可以实现横向的列表
|
|
||||||
|
|
||||||
### 自定义组件
|
|
||||||
当页面结构很复杂的时候,如果我们把原生组件都堆积在一起
|
|
||||||
就会产生非常多的嵌套结构,造成代码难以维护
|
|
||||||
所以就很有必要创建自定义的组件,进行封装和重用
|
|
||||||
```dart
|
|
||||||
class MyList extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(child:
|
|
||||||
ListView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
width: 150,
|
|
||||||
color: Colors.pinkAccent,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 150,
|
|
||||||
color: Colors.blue,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 150,
|
|
||||||
color: Colors.lightGreen,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
width: 150,
|
|
||||||
color: Colors.amber,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
height: 100,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
封装的组件就是这样一个继承`StatelessWidget`的类
|
|
||||||
可以放在任意一个需要使用Widget的地方
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
---
|
|
||||||
title: Flutter动态列表
|
|
||||||
date: 2019-11-06 20:59:37
|
|
||||||
tags:
|
|
||||||
- flutter
|
|
||||||
- dart
|
|
||||||
categories:
|
|
||||||
- flutter
|
|
||||||
---
|
|
||||||
|
|
||||||
ListView提供了`ListView.builder`构造方法用来动态构建列表
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class MyList extends StatelessWidget {
|
|
||||||
final List<String> items;
|
|
||||||
MyList({@required this.items});
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(child:
|
|
||||||
ListView.builder(
|
|
||||||
itemCount: items.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return ListTile(title: Text(items[index]));
|
|
||||||
}
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
`@required`注解表示该参数是必须要传的
|
|
||||||
这里创建一个List<String>类型的属性来存放数据
|
|
||||||
|
|
||||||
调用时可以使用`List.generate`这个List的命名构造方法来快速创建一个列表
|
|
||||||
比如
|
|
||||||
```dart
|
|
||||||
List<String>.generate(100, (index) => 'item $index')
|
|
||||||
```
|
|
||||||
|
|
||||||
### 网格布局
|
|
||||||
网格布局需要使用`GridView`组件,类似于css当中的grid布局方式
|
|
||||||
```dart
|
|
||||||
GridView.builder(
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 4, // 横轴元素数量
|
|
||||||
mainAxisSpacing: 5, // 纵轴间距
|
|
||||||
crossAxisSpacing: 10 // 横轴间距
|
|
||||||
),
|
|
||||||
itemBuilder: (context, index){
|
|
||||||
return Text(this.items[index]);
|
|
||||||
},
|
|
||||||
itemCount: this.items.length,
|
|
||||||
),
|
|
||||||
```
|
|
||||||
可以得到一个网格布局
|
|
||||||

|
|
||||||
`SliverGridDelegateWithFixedCrossAxisCount`其实就是固定横轴元素数量的布局方式
|
|
||||||
crossAxisCount是其必传参数
|
|
||||||
还有一种是指定列宽度根据容器大小自适应的布局`SliverGridDelegateWithMaxCrossAxisExtent`
|
|
||||||
maxCrossAxisExtent是其必传参数
|
|
||||||
|
|
||||||
### 交互操作
|
|
||||||
处理手势可以使用`GestureDetector`组件,它是可以添加手势的一个widget
|
|
||||||
其中包含所有的交互方式,比如触碰 长按 滑动等等
|
|
||||||
这个组件当中包含的子组件就可以响应该组件上定义的交互事件
|
|
||||||
```dart
|
|
||||||
GestureDetector(
|
|
||||||
onTap: (){
|
|
||||||
print('onTap');
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
height: 100,
|
|
||||||
width: 200,
|
|
||||||
color: Colors.pinkAccent,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
这样点击这个Container元素的时候,就可以执行指定的函数
|
|
||||||
|
|
||||||
|
|
||||||
### 接口调用
|
|
||||||
Flutter项目当中根目录下有一个`pubspec.yaml`文件,是对该项目的描述,以及一些依赖的引入
|
|
||||||
类似于nodejs项目的package.json文件的作用
|
|
||||||
|
|
||||||
dart的第三方模块可以到[Dart Package](https://pub.dev/)搜索
|
|
||||||
```yaml
|
|
||||||
dependencies:
|
|
||||||
flutter:
|
|
||||||
sdk: flutter
|
|
||||||
http: ^0.12.0+2 # 加入http模块的引入
|
|
||||||
# The following adds the Cupertino Icons font to your application.
|
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
|
||||||
cupertino_icons: ^0.1.2
|
|
||||||
```
|
|
||||||
之后可以在代码当中使用该模块
|
|
||||||
```dart
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
void httpGet(Function callback, [Function errCallback]) async {
|
|
||||||
try {
|
|
||||||
http.Response response = await http.get('https://www.example.com/');
|
|
||||||
if(callback != null) {
|
|
||||||
callback(response.body);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if(errCallback != null) {
|
|
||||||
errCallback(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**http.get** 返回的是一个Future<String>
|
|
||||||
dart语言的执行本身也是单线程的,与js非常类似
|
|
||||||
所以也需要进行异步回调,内部包含await的函数需要声明为async
|
|
||||||
post的调用方式如下
|
|
||||||
```dart
|
|
||||||
http.Response res = await http.post(url, body: params);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JSON解析
|
|
||||||
需要引入`dart:convert`模块
|
|
||||||
|
|
||||||
```dart
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
var data = json.decode(jsonStr);
|
|
||||||
print(data['total']);
|
|
||||||
```
|
|
||||||
JSON的序列化使用`json.encode`即可
|
|
||||||
@ -1,253 +0,0 @@
|
|||||||
---
|
|
||||||
title: Flutter路由
|
|
||||||
date: 2019-11-10 10:06:41
|
|
||||||
tags:
|
|
||||||
- flutter
|
|
||||||
- dart
|
|
||||||
categories:
|
|
||||||
- flutter
|
|
||||||
---
|
|
||||||
|
|
||||||
使用`MaterialPageRoute`也就是页面路由,可以实现不同Widget之间的跳转
|
|
||||||
页面只是一个全屏的Widget
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
先写好另一个页面的Widget
|
|
||||||
```dart
|
|
||||||
class DetailPage extends StatefulWidget {
|
|
||||||
DetailPage({Key key, this.title}) : super(key: key);
|
|
||||||
final String title;
|
|
||||||
@override
|
|
||||||
_DetailPageState createState() => _DetailPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DetailPageState extends State<DetailPage> {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(widget.title),
|
|
||||||
),
|
|
||||||
body: Text('这里是另一个页面')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
简单的页面跳转
|
|
||||||
```dart
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_){
|
|
||||||
return DetailPage(title: 'detail page');
|
|
||||||
}));
|
|
||||||
```
|
|
||||||
既然是push,那么就是个栈模型了,跳转到另一个页面相当于入栈
|
|
||||||
如果要返回到上一级页面可以这样做,pop相当于是出栈
|
|
||||||
```dart
|
|
||||||
Navigator.pop(context);
|
|
||||||
```
|
|
||||||
### 命名路由
|
|
||||||
可以在MaterialApp当中传入routes参数,该参数是一个Map对象
|
|
||||||
key是路由名称,value是返回组件对象的函数
|
|
||||||
```dart
|
|
||||||
class MyApp extends StatelessWidget {
|
|
||||||
// This widget is the root of your application.
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'Flutter Demo',
|
|
||||||
theme: ThemeData(
|
|
||||||
primarySwatch: Colors.brown,
|
|
||||||
),
|
|
||||||
routes: {
|
|
||||||
'/' : (context) => MyHomePage(title: 'Flutter Demo Home Page'),
|
|
||||||
'/detail' : (context) => DetailPage(title: 'detail page')
|
|
||||||
},
|
|
||||||
initialRoute: '/',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
> 通常的习惯当然是把routes这部分封装在一个单独的模块当中引入使用
|
|
||||||
|
|
||||||
命名路由的跳转
|
|
||||||
```dart
|
|
||||||
Navigator.pushNamed(context, '/detail');
|
|
||||||
```
|
|
||||||
这种方式不需要反复构建和销毁组件对象,相对来讲是更好的一种方式
|
|
||||||
|
|
||||||
### 路由传参
|
|
||||||
基本的**Navigator.push**的方式是构建组件,当然可以给组件的构造方法传入参数,比如上面传入的title
|
|
||||||
这个就不需要特别的传参方式了,如果使用命名路由跳转
|
|
||||||
可以传入第三个可选参数arguments
|
|
||||||
```dart
|
|
||||||
Navigator.pushNamed(context, '/detail', arguments: {'id': 1001});
|
|
||||||
```
|
|
||||||
arguments可以是任意的Object
|
|
||||||
|
|
||||||
接收参数方式
|
|
||||||
```dart
|
|
||||||
ModalRoute.of(context).settings.arguments
|
|
||||||
```
|
|
||||||
当然不论传入的是什么,此时获取到的还是Object类型,需要使用`as Map<String, int>`(取决于传入的类型而执行的类型推导)
|
|
||||||
进行强制类型转换
|
|
||||||
|
|
||||||
|
|
||||||
### 组件之间的共享状态
|
|
||||||
除了组件之间的传参,以及路由的传参之外,组件之间通常需要有一些状态需要共享
|
|
||||||
比如一个常见的需求是用户登录之后,在每个页面上都能看到当前登录的用户是什么名字
|
|
||||||
|
|
||||||
需要使用到的是`provider`这个第三方模块
|
|
||||||
|
|
||||||
先定义Model
|
|
||||||
```dart
|
|
||||||
// user.dart
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'user.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class User {
|
|
||||||
User({this.username, this.createdAt, this.updatedAt});
|
|
||||||
|
|
||||||
String username;
|
|
||||||
String createdAt;
|
|
||||||
String updatedAt;
|
|
||||||
|
|
||||||
factory User.fromJson(Map<String,dynamic> json) => _$UserFromJson(json);
|
|
||||||
Map<String, dynamic> toJson() => _$UserToJson(this);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
还有
|
|
||||||
```dart
|
|
||||||
// user.g.dart
|
|
||||||
part of 'user.dart';
|
|
||||||
|
|
||||||
User _$UserFromJson(Map<String, dynamic> json) {
|
|
||||||
return User(
|
|
||||||
username: json['username'] as String,
|
|
||||||
createdAt: json['createdAt'] as String,
|
|
||||||
updatedAt: json['updatedAt'] as String,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
|
|
||||||
'username': instance.username,
|
|
||||||
'createdAt': instance.createdAt,
|
|
||||||
'updatedAt': instance.updatedAt,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
为了能够在启动时读取上次运行的缓存
|
|
||||||
需要使用`shared_preferences`这个第三方模块
|
|
||||||
|
|
||||||
定义全局对象
|
|
||||||
```dart
|
|
||||||
// global.dart
|
|
||||||
class Global {
|
|
||||||
static SharedPreferences _prefs;
|
|
||||||
static Profile profile = Profile();
|
|
||||||
|
|
||||||
// 是否为release版
|
|
||||||
static bool get isRelease => bool.fromEnvironment("dart.vm.product");
|
|
||||||
|
|
||||||
//初始化全局信息,会在APP启动时执行
|
|
||||||
static Future init() async {
|
|
||||||
_prefs = await SharedPreferences.getInstance();
|
|
||||||
var _profile = _prefs.getString("profile");
|
|
||||||
if (_profile != null) {
|
|
||||||
try {
|
|
||||||
profile = Profile.fromJson(jsonDecode(_profile));
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有缓存策略,设置默认缓存策略
|
|
||||||
profile.cache = profile.cache ?? CacheConfig()
|
|
||||||
..enable = true
|
|
||||||
..maxAge = 3600
|
|
||||||
..maxCount = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 持久化Profile信息
|
|
||||||
static saveProfile() =>
|
|
||||||
_prefs.setString("profile", jsonEncode(profile.toJson()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// profile.dart
|
|
||||||
@JsonSerializable()
|
|
||||||
class Profile {
|
|
||||||
Profile({this.user, this.token, this.theme, this.cache, this.lastLogin,
|
|
||||||
this.locale});
|
|
||||||
|
|
||||||
User user;
|
|
||||||
String token;
|
|
||||||
CacheConfig cache;
|
|
||||||
String lastLogin;
|
|
||||||
String locale;
|
|
||||||
|
|
||||||
factory Profile.fromJson(Map<String,dynamic> json) => _$ProfileFromJson(json);
|
|
||||||
Map<String, dynamic> toJson() => _$ProfileToJson(this);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
profile当中可以持有User对象
|
|
||||||
这种情况下就需要在应用启动时就初始化Global对象
|
|
||||||
```dart
|
|
||||||
void main() => Global.init().then((e) => runApp(MyApp()));
|
|
||||||
```
|
|
||||||
自定义Notifier
|
|
||||||
```dart
|
|
||||||
class ProfileChangeNotifier extends ChangeNotifier {
|
|
||||||
Profile get _profile => Global.profile;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void notifyListeners() {
|
|
||||||
Global.saveProfile(); //保存Profile变更
|
|
||||||
super.notifyListeners(); //通知依赖的Widget更新
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 用户状态在登录状态发生变化时更新、通知其依赖项
|
|
||||||
class UserModel extends ProfileChangeNotifier {
|
|
||||||
User get user => _profile.user;
|
|
||||||
|
|
||||||
// APP是否登录(如果有用户信息,则证明登录过)
|
|
||||||
bool get isLogin => user != null;
|
|
||||||
|
|
||||||
//用户信息发生变化,更新用户信息并通知依赖它的子孙Widgets更新
|
|
||||||
set user(User user) {
|
|
||||||
if (user?.username != _profile.user?.username) {
|
|
||||||
_profile.lastLogin = _profile.user?.username;
|
|
||||||
_profile.user = user;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
根组件需要使用InheritedProvider进行包装
|
|
||||||
```dart
|
|
||||||
class MyApp extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return InheritedProvider<UserModel>(
|
|
||||||
value: UserModel(),
|
|
||||||
child: MaterialApp(
|
|
||||||
title: 'My App',
|
|
||||||
theme: ThemeData(
|
|
||||||
primarySwatch: Colors.pink,
|
|
||||||
),
|
|
||||||
routes: {
|
|
||||||
'login' : (context) => Login(title: '登录'),
|
|
||||||
'home' : (context) => Home(title: '首页')
|
|
||||||
},
|
|
||||||
initialRoute: 'login',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
共享状态的更新,比如在登录时,需要把当前用户的信息写入profile,并且传播到各个组件当中
|
|
||||||
这里简单写入一个username作为例子
|
|
||||||
```dart
|
|
||||||
Provider.of<UserModel>(context).user = User.fromJson({'username': _usernameController.text});
|
|
||||||
```
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
---
|
|
||||||
title: Flutter踩坑记录
|
|
||||||
date: 2019-11-30 22:31:53
|
|
||||||
tags:
|
|
||||||
- flutter
|
|
||||||
- dart
|
|
||||||
categories:
|
|
||||||
- flutter
|
|
||||||
---
|
|
||||||
|
|
||||||
### 数据绑定
|
|
||||||
使用`setState`方法对底层数据进行修改并且动态渲染视图
|
|
||||||
比如需要从接口获取数据显示到页面当中
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Response response = await http.get('common/items');
|
|
||||||
setState((){
|
|
||||||
this.loading = false;
|
|
||||||
this.items = response.data;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
采用这种方式进行赋值才可以执行动态绑定
|
|
||||||
否则不会进行视图的重新渲染
|
|
||||||
该方法继承自抽象类`State`
|
|
||||||
|
|
||||||
### 获取屏幕的尺寸
|
|
||||||
```dart
|
|
||||||
final Size screenSize = MediaQuery.of(context).size;
|
|
||||||
```
|
|
||||||
Size对象当中包含width和height属性,可以用于根据屏幕的高度控制控件的大小
|
|
||||||
|
|
||||||
### 下拉刷新与滚动更新
|
|
||||||
下拉刷新直接使用`RefreshIndicator`控件包装
|
|
||||||
当滚动到顶部并且进行下滑动作时就可以执行指定方法
|
|
||||||
```dart
|
|
||||||
RefreshIndicator(
|
|
||||||
onRefresh: (){
|
|
||||||
return http.get('common/photos').then((response) {
|
|
||||||
setState((){
|
|
||||||
this.loading = false;
|
|
||||||
this.items = response.data
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Container(/*省略内部子组件代码*/),
|
|
||||||
),
|
|
||||||
```
|
|
||||||
需要注意的是`onRefresh`函数的返回值必须是`Future<void>`类型,用于执行异步回调
|
|
||||||
|
|
||||||
**滚动更新**需要给组件绑定`ScrollController`
|
|
||||||
并且在组件初始化时绑定滚动监听
|
|
||||||
```dart
|
|
||||||
// 滚动控制器
|
|
||||||
ScrollController _scrollController = ScrollController();
|
|
||||||
|
|
||||||
ListView(
|
|
||||||
controller: this._scrollController,
|
|
||||||
),
|
|
||||||
// 省略其他代码...
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_scrollController.addListener(() { // 绑定滚动监听事件
|
|
||||||
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
|
|
||||||
// TODO 滚动到底部需要执行的操作
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 区分开发环境与生产环境
|
|
||||||
```dart
|
|
||||||
bool.fromEnvironment('dart.vm.product');
|
|
||||||
```
|
|
||||||
true代表是生产环境,false代表开发环境
|
|
||||||
|
|
||||||
### 弹性布局
|
|
||||||
Flutter也实现了类似于CSS当中的flex弹性容器布局
|
|
||||||
可以按照比例分配子元素的宽度或高度
|
|
||||||
```dart
|
|
||||||
Flex(children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
flex: 1,
|
|
||||||
child:Text('内容', style: TextStyle(fontSize: 18), textAlign: TextAlign.center)
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
flex: 4,
|
|
||||||
child: TextFormField(
|
|
||||||
validator: (v) {
|
|
||||||
return v.trim().isNotEmpty ? null : '必须输入内容';
|
|
||||||
},
|
|
||||||
onChanged: (value) {
|
|
||||||
this.fromData['content'] = value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
direction: Axis.horizontal
|
|
||||||
)
|
|
||||||
```
|
|
||||||
这是一个常规的表单项的结构,分为一个标题和一个输入框
|
|
||||||
并且指定子元素排列方向为横向
|
|
||||||
两者的宽度之比为1:4
|
|
||||||
|
|
||||||
@ -1,228 +0,0 @@
|
|||||||
---
|
|
||||||
title: dart初见(1)
|
|
||||||
date: 2019-09-28 12:08:40
|
|
||||||
tags:
|
|
||||||
- dart
|
|
||||||
categories:
|
|
||||||
- flutter
|
|
||||||
---
|
|
||||||
|
|
||||||
Flutter是谷歌推出的跨平台移动端开发框架
|
|
||||||
它使用dart这门小众语言作为开发语言
|
|
||||||
想到自己在移动端开发这块的技能还相对薄弱,所以最近学习了一下这门语言,为学习Flutter框架稍作准备
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
dart的代码语句末尾必须写分号
|
|
||||||
学习一门编程语言,当然要先了解这门语言的基础设施
|
|
||||||
|
|
||||||
### 变量与数据类型
|
|
||||||
dart语言有类型推导机制,但仍然是强类型语言
|
|
||||||
未初始化的变量值均为null,如果一个变量未在定义时被初始化,那么它的类型就不会被确定
|
|
||||||
可以指向任意类型的对象
|
|
||||||
```dart
|
|
||||||
void main() {
|
|
||||||
var num1 = 10;
|
|
||||||
num1 = 'hello'; // 错误
|
|
||||||
print(num1);
|
|
||||||
|
|
||||||
var num2;
|
|
||||||
num2 = 10;
|
|
||||||
num2 = 'hello'; // 正确
|
|
||||||
print(num1);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
用`var`来定义变量会执行类型推导
|
|
||||||
也可以用`int`等类型名称直接声明变量类型,比如`int a = 10;`
|
|
||||||
|
|
||||||
#### 内置的数据类型
|
|
||||||
+ **数值型-Number** - 用`num`表示,包含`int`和`double`两种子类型,分别表示整数和浮点数
|
|
||||||
+ **字符串-String** - 用`String`表示
|
|
||||||
+ **布尔型-Boolean** - 用`bool`表示,分为true和false
|
|
||||||
+ **列表-List** - 用`List`表示,用诸如 **\[10,'ab',15.9\]** 的形式声明一个列表(类似python)
|
|
||||||
前面加const表示不可变的List
|
|
||||||
+ **无序集合-Set** - 用`Set`表示,用诸如 **{10,'ab',15.9}** 的形式声明一个无序集合,集合中的元素是唯一的
|
|
||||||
+ **键值对-Map** - 用`Map`表示,用诸如 **{'a':10, 'b':20}** 的形式声明一个键值对的集合
|
|
||||||
+ Runes、Symbols
|
|
||||||
|
|
||||||
这些本质上都是dart内置的类,比如int和double这两个类就是继承自num类的
|
|
||||||
|
|
||||||
#### final与const
|
|
||||||
用final修饰的变量,必须在定义时将其初始化,其值在初始化后不可改变;const用来定义常量
|
|
||||||
|
|
||||||
它们的区别在于,const比final更加严格。final只是要求变量在初始化后值不变,但通过final,我们无法在编译时(运行之前)知道这个变量的值
|
|
||||||
而const所修饰的是编译时常量,我们在编译时就已经知道了它的值,显然,它的值也是不可改变的。
|
|
||||||
```dart
|
|
||||||
void main() {
|
|
||||||
final int m1 = 60;
|
|
||||||
final int m2 = func(); // 正确
|
|
||||||
const int n1 = 42;
|
|
||||||
const int n2 = func(); // 错误
|
|
||||||
}
|
|
||||||
|
|
||||||
int func() {
|
|
||||||
return 500;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
上面的例子当中,显然编译期是无法知道func函数的执行结果的,所以无法用const变量接收其返回值
|
|
||||||
|
|
||||||
### 数值型操作
|
|
||||||
|
|
||||||
针对数值的运算符,大部分都和其他语言差不多
|
|
||||||
有差别的是多一个`~/`运算符,它表示取整的除法,直接使用`/`是保留小数部分的
|
|
||||||
`%`取余运算是不取整的
|
|
||||||
|
|
||||||
常用属性
|
|
||||||
+ `isNaN` - 0/0的运算会得到一个NaN,可以用来做判断
|
|
||||||
+ `isEven` - 是否是偶数
|
|
||||||
+ `isOdd` - 是否是奇数
|
|
||||||
|
|
||||||
常用方法
|
|
||||||
+ `abs()` - 取绝对值
|
|
||||||
+ `round()` - 四舍五入
|
|
||||||
+ `floor()` - 向下取整
|
|
||||||
+ `ceil()` - 向上取整
|
|
||||||
+ `toInt()` - 转换为int类型
|
|
||||||
+ `toDouble()` - 转换为double类型
|
|
||||||
|
|
||||||
### 字符串操作
|
|
||||||
1. 使用单引号或双引号来表示字符串常量
|
|
||||||
2. 三个单引号或双引号可以表示多行字符串
|
|
||||||
3. 使用`r`创建原始raw字符串
|
|
||||||
|
|
||||||
前两种都很简单,和js也类似
|
|
||||||
第三种的意思是是否对转义字符进行处理
|
|
||||||
```dart
|
|
||||||
String str1 = 'a\nb';
|
|
||||||
String str2 = r'a\nb';
|
|
||||||
|
|
||||||
print(str1); // a b
|
|
||||||
print(str2); // a\tb
|
|
||||||
```
|
|
||||||
第一个转义字符是起作用的,第二个则不会对转义字符进行处理
|
|
||||||
|
|
||||||
运算符
|
|
||||||
+ `+` - 字符串拼接(注意字符串不能与非字符串类型的常量或变量进行拼接)
|
|
||||||
+ `*` - 后面跟的是次数,表示把该字符串重复若干次
|
|
||||||
+ `==` - 判断是否相同
|
|
||||||
+ `[]` - 获取字符串指定索引位置的字符(从0开始)
|
|
||||||
|
|
||||||
插值表达式`${expression}`
|
|
||||||
```dart
|
|
||||||
int a = 10;
|
|
||||||
int b = 20;
|
|
||||||
print('a=$a'); // 如果只是一个变量 也可以不写大括号
|
|
||||||
print('a+b=${a+b}');
|
|
||||||
```
|
|
||||||
常用属性
|
|
||||||
+ `length` - 字符串长度
|
|
||||||
+ `isEmpty` - 是否是空串
|
|
||||||
+ `isNotEmpty` - 是否不是空串
|
|
||||||
|
|
||||||
常用方法
|
|
||||||
+ `contains(str)`,`subString(start,end)` - 是否包含某个子串,取子串(end不传则截取到末尾)
|
|
||||||
+ `startWith(str)`,`endsWith(str)` - 是否以某个字符串开头/结尾
|
|
||||||
+ `indexOf(str)`,`lastIndexOf(str)` - 子串第一次/最后一次出现的位置
|
|
||||||
+ `toUpperCase()`,`toLowerCase()` - 转换为大写/小写
|
|
||||||
+ `trim()`,`trimLeft()`,`trimRight()` - 去除首/尾的空白字符
|
|
||||||
+ `split(str)` - 分割字符串,得到一个List对象
|
|
||||||
+ `replaceAll(form,str)` - 正则替换
|
|
||||||
replace有一系列的方法
|
|
||||||
第一个参数可以是字符串也可以是正则对象
|
|
||||||
```dart
|
|
||||||
String str1 = 'ab123cd45';
|
|
||||||
RegExp numReg = new RegExp(r'\d+'); // 也可以写 '\\d+'
|
|
||||||
print(str1.replaceAll(numReg, '..')); // ab..cd..
|
|
||||||
```
|
|
||||||
|
|
||||||
### List操作
|
|
||||||
一个List在定义之后,可以使用类似数组操作的方式用`[]`获取指定下标的元素以及修改指定元素
|
|
||||||
使用`length`属性获取List的长度
|
|
||||||
但是不能像js那样对越界的下标直接赋值
|
|
||||||
|
|
||||||
常用方法
|
|
||||||
+ `add(value)`,`insert(index,value)` - 添加元素/在指定位置添加元素
|
|
||||||
+ `remove(value)`,`removeAt(index)` - 移除指定元素/指定位置的元素
|
|
||||||
+ `indexOf(value)`,`lastIndexOf(value)` - 查找元素第一次/最后一次出现的位置,找不到返回-1
|
|
||||||
+ `sort(func)` - 以指定规则排序
|
|
||||||
> dart同样支持函数式编程
|
|
||||||
sort就需要传入一个函数对象,这个函数需要返回一个整数
|
|
||||||
正数表示大于,负数表示小于,0表示等于
|
|
||||||
```dart
|
|
||||||
List list1 = ['aa','b','cccc','ddd'];
|
|
||||||
list1.sort((a,b) => a.length.compareTo(b.length));//[b, aa, ddd, cccc]
|
|
||||||
```
|
|
||||||
+ `shuffle()` - 随机乱序
|
|
||||||
+ `asMap()` - 转换为下标为key,元素为value的键值对集合
|
|
||||||
+ `forEach(func)` - 对List执行遍历(传入一个函数)
|
|
||||||
|
|
||||||
### Map操作
|
|
||||||
使用`[]`的方式获取或修改Map当中的元素
|
|
||||||
`length`可以获取Map的键值对数量
|
|
||||||
`keys`和`values`可以获取键和值的集合
|
|
||||||
|
|
||||||
|
|
||||||
常用方法
|
|
||||||
+ `isEmpty()`,`isNotEmpty()` - 判断是否(不)为空
|
|
||||||
+ `containsKey(key)`,`containsValue(value)` - 是否包含某个键/值
|
|
||||||
+ `remove(key)` - 按照key移除某个键值对
|
|
||||||
+ `forEach(func)` - 遍历Map
|
|
||||||
|
|
||||||
|
|
||||||
### dynamic与泛型
|
|
||||||
dart语言当中使用`dynamic`来表示动态类型
|
|
||||||
```dart
|
|
||||||
var a;
|
|
||||||
a = 10;
|
|
||||||
a = 'abc'; // a就相当于是dynamic类型的一个变量
|
|
||||||
// 也可以这样定义变量
|
|
||||||
dynamic b = 20;
|
|
||||||
b = 'cde';
|
|
||||||
```
|
|
||||||
对于集合类型,可以指定泛型,或者使用dynamic来表示任意类型
|
|
||||||
比如
|
|
||||||
```dart
|
|
||||||
Map<String, dynamic> map = new Map();
|
|
||||||
```
|
|
||||||
就表示这个Map的key必须是字符串,value可以是任意类型
|
|
||||||
|
|
||||||
|
|
||||||
### 条件表达式与运算符
|
|
||||||
dart当中除了常规的赋值与三目运算符之外
|
|
||||||
还有`??`运算符以及`??=`赋值运算符
|
|
||||||
|
|
||||||
```dart
|
|
||||||
int a = 10;
|
|
||||||
a ??= 20;
|
|
||||||
print(a); // 10
|
|
||||||
|
|
||||||
int b;
|
|
||||||
int c = b ?? 30;
|
|
||||||
print(c); // 30
|
|
||||||
```
|
|
||||||
+ `??=` 代表为左值空的时候就执行赋值,否则就什么都不做
|
|
||||||
+ `expr1 ?? expr2` 代表判断expr1是否为null,如果为null就取expr2的值,否则就取expr1的值
|
|
||||||
|
|
||||||
### switch语句的差异
|
|
||||||
dart当中的switch与其他语言的用法基本一致
|
|
||||||
有一点差别就是
|
|
||||||
> 不是switch当中的最后一个case
|
|
||||||
其最后一个语句必须是下列关键字其中之一
|
|
||||||
`break`, `continue`, `rethrow`, `return` or `throw`
|
|
||||||
|
|
||||||
+ `continue`后面必须跟一个label的名称,label必须是在这个switch当中某个位置指定的标签
|
|
||||||
```dart
|
|
||||||
void func(String name) {
|
|
||||||
switch(name) {
|
|
||||||
case 'Sookie' :
|
|
||||||
print(1);
|
|
||||||
continue label1;
|
|
||||||
case 'Alice' :
|
|
||||||
print(2);
|
|
||||||
break;
|
|
||||||
label1:
|
|
||||||
case 'Lily' :
|
|
||||||
print(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
+ `rethrow`可以写在catch块当中,代表在捕获异常的同时进行传播
|
|
||||||
@ -1,292 +0,0 @@
|
|||||||
---
|
|
||||||
title: dart初见(2)
|
|
||||||
date: 2019-10-06 21:08:37
|
|
||||||
tags:
|
|
||||||
- dart
|
|
||||||
categories:
|
|
||||||
- flutter
|
|
||||||
---
|
|
||||||
|
|
||||||
### 函数相关
|
|
||||||
dart当中函数也是一个对象,具备共同的抽象类`Function`
|
|
||||||
因为是抽象类,所以不能直接用其执行new创建对象
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
#### 可选参数
|
|
||||||
dart在定义函数的时候可以在形参表的末尾添加可选参数
|
|
||||||
有命名参数和匿名参数两种形式
|
|
||||||
```dart
|
|
||||||
void main() {
|
|
||||||
func1('Sookie', c: true, b: 10);
|
|
||||||
func2('Sookie', 20, false);
|
|
||||||
}
|
|
||||||
// 命名可选参数
|
|
||||||
void func1(String a, {int b, bool c}) {
|
|
||||||
print('a=${a},b=${b},c=${c}');
|
|
||||||
}
|
|
||||||
// 匿名可选参数
|
|
||||||
void func2(String a, [int b = 100 ,bool c]) {
|
|
||||||
// 可以给可选参数指定默认值
|
|
||||||
print('a=${a},b=${b},c=${c}');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
命名的可选参数因为传参时指定形参名称,所以就不会强制顺序的匹配了
|
|
||||||
可选参数的声明必须在形参表的末尾
|
|
||||||
|
|
||||||
|
|
||||||
#### 匿名函数和箭头函数
|
|
||||||
**匿名函数**
|
|
||||||
```
|
|
||||||
(参数1, 参数2 ...) {
|
|
||||||
函数体
|
|
||||||
}
|
|
||||||
```
|
|
||||||
匿名函数不能声明返回值类型,或者说它的返回值是任意类型的,相当于是`dynamic`
|
|
||||||
> dynamic并不算一种类型,只是作为一个关键字表示动态类型
|
|
||||||
就像var也是关键字,但是会执行类型推导
|
|
||||||
|
|
||||||
**箭头函数**
|
|
||||||
```
|
|
||||||
(参数1, 参数2 ...) => 返回值
|
|
||||||
```
|
|
||||||
箭头函数只能有一个表达式作为返回值,而不能有其他语句
|
|
||||||
函数对象都可以使用变量进行接收
|
|
||||||
|
|
||||||
#### 级联调用
|
|
||||||
dart当中使用`..`来实现级联调用
|
|
||||||
```dart
|
|
||||||
List list1 = [10,15,20];
|
|
||||||
list1..add(19)..remove(10);
|
|
||||||
// 相当于
|
|
||||||
List list2 = [10,15,20];
|
|
||||||
list2.add(19);
|
|
||||||
list2.remove(10);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 面向对象
|
|
||||||
使用`class`关键字来声明一个类,创建对象时`new`关键字可以省略
|
|
||||||
dart中不支持函数重载
|
|
||||||
|
|
||||||
可以使用`.`来访问对象的成员
|
|
||||||
当然如果变量是null会报空指针的错误
|
|
||||||
可以判断是否为null
|
|
||||||
也可以使用`?.`操作符,相当于自带判断是否为null,如果是null就不执行调用
|
|
||||||
|
|
||||||
类如果实现了`call`函数,那么它的实例就可以直接当做函数来执行调用
|
|
||||||
```dart
|
|
||||||
void main() {
|
|
||||||
Demo demo = new Demo();
|
|
||||||
demo('hello');
|
|
||||||
}
|
|
||||||
|
|
||||||
class Demo{
|
|
||||||
void call(String msg) {
|
|
||||||
print(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
#### 计算属性
|
|
||||||
对于不是固定值,而是需要使用一些逻辑获得计算结果的情况
|
|
||||||
我们可以将其定义为类当中的一个函数,也可以使用计算属性
|
|
||||||
|
|
||||||
计算属性分为`get`和`set`两种
|
|
||||||
```dart
|
|
||||||
void main() {
|
|
||||||
Rectangle rect = new Rectangle();
|
|
||||||
rect.width = 10.9;
|
|
||||||
rect.height = 20.3;
|
|
||||||
print(rect.area); // 221.27
|
|
||||||
|
|
||||||
rect.area = 100;
|
|
||||||
print(rect.width); // 5.0
|
|
||||||
}
|
|
||||||
|
|
||||||
class Rectangle {
|
|
||||||
num width, height;
|
|
||||||
|
|
||||||
num get area {
|
|
||||||
return width * height;
|
|
||||||
}
|
|
||||||
void set area(num value) {
|
|
||||||
this.height = 20;
|
|
||||||
this.width = value / 20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
> 它本身当然是个函数,也可以使用箭头函数的写法
|
|
||||||
|
|
||||||
#### 构造函数
|
|
||||||
定义一个类时,如果不写构造函数,会默认提供一个无参的构造函数
|
|
||||||
支持匿名构造函数与命名构造函数两种
|
|
||||||
构造函数也不能被重载,如果要实现一个类有多个构造函数,就必须使用命名构造函数
|
|
||||||
```dart
|
|
||||||
class Person {
|
|
||||||
String name;
|
|
||||||
int age;
|
|
||||||
// 匿名构造函数
|
|
||||||
Person(String name, int age) {
|
|
||||||
this.name = name;
|
|
||||||
this.age = age;
|
|
||||||
}
|
|
||||||
// 也可以写作
|
|
||||||
// Person(this.name, this.age);
|
|
||||||
|
|
||||||
// 命名构造函数
|
|
||||||
Person.withName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
命名构造函数调用时写作 **new Person.withName('sookie')**
|
|
||||||
|
|
||||||
对于`final`修饰的类属性,想要在构造函数进行初始化,只能使用这种语法糖的形式来写
|
|
||||||
```dart
|
|
||||||
class Person {
|
|
||||||
final String name;
|
|
||||||
int age;
|
|
||||||
|
|
||||||
Person(this.name, this.age);
|
|
||||||
Person.withName(this.name);
|
|
||||||
// 下面的方式是报错的
|
|
||||||
// Person(String name, int age) {
|
|
||||||
// this.name = name;
|
|
||||||
// this.age = age;
|
|
||||||
// }
|
|
||||||
// 这种方式当然也报错
|
|
||||||
// Person.withName(String name) {
|
|
||||||
// this.name = name;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
##### 常量对象
|
|
||||||
```dart
|
|
||||||
void main() {
|
|
||||||
const Person p = const Person('Sookie', 18);
|
|
||||||
print(p.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Person {
|
|
||||||
final String name;
|
|
||||||
final int age;
|
|
||||||
const Person(this.name, this.age);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
##### 工厂构造方法
|
|
||||||
dart自带了工厂设计模式的实现,工厂构造方法需要返回该类的实例对象
|
|
||||||
```dart
|
|
||||||
class Logger {
|
|
||||||
String name;
|
|
||||||
static final Map<String, Logger> _cache = new Map();
|
|
||||||
factory Logger(String name) {
|
|
||||||
if(!_cache.containsKey(name)) {
|
|
||||||
_cache[name] = new Logger(name);
|
|
||||||
}
|
|
||||||
return _cache[name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 类型转换
|
|
||||||
使用`as`进行强制类型转换
|
|
||||||
使用`is`和`is!`来判断是否属于某种类型
|
|
||||||
```dart
|
|
||||||
dynamic a = 'abc';
|
|
||||||
if(a is String) {
|
|
||||||
print((a as String).toUpperCase());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
如果不执行强制类型转换,就不能直接调用String上的方法,因为此时a还是动态类型
|
|
||||||
|
|
||||||
|
|
||||||
#### 继承
|
|
||||||
dart当中的继承与java基本类似
|
|
||||||
+ 使用`extends`关键字实现继承,只能单继承,具备多态特性
|
|
||||||
+ 子类会继承父类可见的成员,不会继承构造方法
|
|
||||||
+ 子类可以重写父类的方法(重写的方法上可以添加`@override`注解)
|
|
||||||
+ 所有类都有公共的基类`Object`
|
|
||||||
|
|
||||||
##### 构造函数
|
|
||||||
子类的构造函数会默认隐式调用父类无名无参的构造函数
|
|
||||||
如果父类当中没有,则需要使用super显式调用
|
|
||||||
```dart
|
|
||||||
class Demo1{
|
|
||||||
String name;
|
|
||||||
Demo1(String name) {
|
|
||||||
print('Demo1:${name}');
|
|
||||||
}
|
|
||||||
Demo1.withName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Demo2 extends Demo1 {
|
|
||||||
// 显式调用父类的匿名构造函数
|
|
||||||
Demo2(String name):super(name) {
|
|
||||||
print('Demo2:${name}');
|
|
||||||
}
|
|
||||||
// 显式调用父类的命名构造函数
|
|
||||||
Demo2.withName(String name):super.withName(name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
#### 抽象类与接口
|
|
||||||
抽象类使用`abstract`修饰,跟Java基本一样,可以定义抽象方法或者有具体实现的方法(抽象方法并不需要再加abstract修饰)
|
|
||||||
但是并不存在interface关键字
|
|
||||||
一个类本身就可以当做接口来使用
|
|
||||||
```dart
|
|
||||||
class Demo{
|
|
||||||
String name;
|
|
||||||
void test(String msg) {
|
|
||||||
print(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 如果作为接口使用,就必须重写所有的属性与方法
|
|
||||||
class Demo1 implements Demo {
|
|
||||||
@override
|
|
||||||
String name;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void test(String msg) {
|
|
||||||
// TODO: implement test
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
当然这是非常冗余的,至少属性没什么好重写的
|
|
||||||
所以实际开发当中通常把抽象类当做以往意义上的接口来使用
|
|
||||||
|
|
||||||
#### Mixins
|
|
||||||
使用extends只能实现单继承,Mixins就是多继承的一种实现方式
|
|
||||||
使一个类同时拥有多个类的成员
|
|
||||||
```dart
|
|
||||||
class A {
|
|
||||||
void a() {
|
|
||||||
print('A.a');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class B {
|
|
||||||
void b() {
|
|
||||||
print('B.b');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class C with A,B {
|
|
||||||
//这个类中同时具备A,B两个类的成员
|
|
||||||
}
|
|
||||||
// 如果类中不需要添加其他成员,也可以使用如下简写方式
|
|
||||||
class D = A with B;
|
|
||||||
```
|
|
||||||
如果A和B当中存在同名方法,那么就是根据**with之后的声明顺序**
|
|
||||||
A在前B在后,所以C中实际具备的是B当中的该方法
|
|
||||||
|
|
||||||
需要注意的是with后面跟的类
|
|
||||||
1. **不能**有显式声明的构造函数
|
|
||||||
2. **只能**直接继承自Object(可以省略 extends Object)
|
|
||||||
3. 可以使用implements实现接口
|
|
||||||
|
|
||||||
### 模块化
|
|
||||||
+ 可见性以`library`(库)为单位,差不多可以理解为JavaScript当中的模块
|
|
||||||
+ 每一个dart文件就是一个库
|
|
||||||
+ 以`_`开头声明的类或者函数是在当前库当中私有的,即使其他库引入了该库,也无法在其他库中使用
|
|
||||||
+ 使用`import`来引入一个库文件
|
|
||||||
+ dart的sdk有自带的一套类库,`dart.core`是默认被引入的
|
|
||||||
@ -27,49 +27,3 @@ do
|
|||||||
echo $LINE
|
echo $LINE
|
||||||
done < test.txt
|
done < test.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 通用解压函数
|
|
||||||
`$1`表示函数接收到的第一个参数
|
|
||||||
```bash
|
|
||||||
extract() {
|
|
||||||
if [ -f $1 ] ; then
|
|
||||||
case $1 in
|
|
||||||
*.tar.bz2) tar xjf $1 ;;
|
|
||||||
*.tar.gz) tar xzf $1 ;;
|
|
||||||
*.bz2) bunzip2 $1 ;;
|
|
||||||
*.rar) unrar e $1 ;;
|
|
||||||
*.gz) gunzip $1 ;;
|
|
||||||
*.tar) tar xf $1 ;;
|
|
||||||
*.tbz2) tar xjf $1 ;;
|
|
||||||
*.tgz) tar xzf $1 ;;
|
|
||||||
*.zip) unzip $1 ;;
|
|
||||||
*.Z) uncompress $1 ;;
|
|
||||||
*.7z) 7z x $1 ;;
|
|
||||||
*) echo "$1 cannot be extracted via extract()" ;;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
echo "$1 is not a valid file"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
```
|
|
||||||
执行`extract 文件名`就可以解压任意格式的压缩文件了, 比如**extract nodejs.tar.gz**
|
|
||||||
可以把上述函数添加到`~/.bashrc`当中, 使用bash作为shell的时候会自动先执行该文件
|
|
||||||
这样每次都可以使用了
|
|
||||||
一些命令的别名, 也可以配置在这里面
|
|
||||||
|
|
||||||
#### 命令的别名
|
|
||||||
`alias`命令用于给指定的命令组合指定别名
|
|
||||||
比如
|
|
||||||
```bash
|
|
||||||
alias ls='ls --color=auto'
|
|
||||||
alias ll="ls --color -al"
|
|
||||||
alias grep='grep --color=auto'
|
|
||||||
|
|
||||||
# 查看当前时间
|
|
||||||
alias now='date "+%Y-%m-%d %H:%M:%S"'
|
|
||||||
```
|
|
||||||
此时执行`ll`就相当于执行`ls --color -al`
|
|
||||||
|
|
||||||
如果别名覆盖了原本的命令 ( 比如上面的ls )
|
|
||||||
想使用原本的命令可以在前面加`\`
|
|
||||||
也就是`\ls`
|
|
||||||
@ -80,10 +80,10 @@ docker container run [ImageId]
|
|||||||
# 运行指定的容器
|
# 运行指定的容器
|
||||||
docker container start [ContainerId]
|
docker container start [ContainerId]
|
||||||
|
|
||||||
# 停止指定的容器
|
# 停止指定的镜像
|
||||||
docker container stop [ContainerId]
|
docker container stop [ContainerId]
|
||||||
|
|
||||||
# 强行终止指定的容器
|
# 强行终止指定的镜像
|
||||||
docker container kill [ContainerId]
|
docker container kill [ContainerId]
|
||||||
```
|
```
|
||||||
使用`run`每次都会生成一个新的容器文件 , 如果要复用指定的容器 , 可以使用`start`
|
使用`run`每次都会生成一个新的容器文件 , 如果要复用指定的容器 , 可以使用`start`
|
||||||
|
|||||||
@ -1,128 +0,0 @@
|
|||||||
---
|
|
||||||
title: linux守护进程
|
|
||||||
date: 2020-01-12 19:31:20
|
|
||||||
tags:
|
|
||||||
- linux
|
|
||||||
categories:
|
|
||||||
- linux
|
|
||||||
---
|
|
||||||
|
|
||||||
多数的服务端程序都需要在服务器上持续运行
|
|
||||||
比如使用nodejs运行一个http服务
|
|
||||||
<!-- more -->
|
|
||||||
```javascript
|
|
||||||
// server.js
|
|
||||||
const http = require('http')
|
|
||||||
http.createServer(function(req, res){
|
|
||||||
console.log('收到一个请求')
|
|
||||||
res.writeHead(200, {'Content-Type': 'text/plain'})
|
|
||||||
res.end('Hello')
|
|
||||||
}).listen(3000)
|
|
||||||
```
|
|
||||||
现在运行,直接执行`node server.js`即可
|
|
||||||
但是按照这种方式运行一旦断开ssh的链接,该进程就会停止
|
|
||||||
|
|
||||||
> 以这种方式启动的进程,按`Ctrl+C`可以中断该进程
|
|
||||||
|
|
||||||
### 前台任务与后台任务
|
|
||||||
上面这种运行的方式,实际是创建了一个`前台任务`
|
|
||||||
在执行的命令后面加上`&`符号,是将其作为一个`后台任务`
|
|
||||||
```bash
|
|
||||||
node server.js &
|
|
||||||
```
|
|
||||||
如果要让正在执行的前台任务转化为后台任务
|
|
||||||
可以先按`Ctrl+Z`然后输入`bg`
|
|
||||||
|
|
||||||
> `Ctrl+Z`的作用是将当前的前台任务转换为后台任务,但是处于暂停状态,在输入bg之前是不能访问的
|
|
||||||
|
|
||||||

|
|
||||||
`jobs`命令可以查看当前session的后台任务列表
|
|
||||||
|
|
||||||
后台任务有如下特点
|
|
||||||
1. 继承当前会话(session)的标准输出(stdout)与错误输出(stderr)
|
|
||||||
上图中的执行方式并没有对输出进行重定向,所以转换为后台任务后依然会在命令行输出
|
|
||||||
2. **不再**继承当前会话(session)的标准输入(stdin)
|
|
||||||
上图中的执行方式也没有对标准输入进行重定向,所以标准输入仍然来自于命令行,当然这段代码当中并没有从标准输入获取内容的操作
|
|
||||||
如果有,该进程就会一直暂停执行
|
|
||||||
|
|
||||||
### SIGNUP 信号
|
|
||||||
变成后台任务之后,该进程是否会继续执行,Linux的机制是这样的
|
|
||||||
1. 用户准备断开session(比如logout命令或者exit命令)
|
|
||||||
2. 系统向该session发出`SIGNUP`信号
|
|
||||||
3. session将`SIGNUP`信号发给所有子进程
|
|
||||||
4. 子进程收到`SIGNUP`信号后,自动中断
|
|
||||||
|
|
||||||
概括来说,就是进程是否会中断退出,就是取决于其是否会收到SIGNUP信号
|
|
||||||
|
|
||||||
后台任务是否会收到SIGNUP信号,是由`huponexit`参数决定的
|
|
||||||
```bash
|
|
||||||
shopt | grep huponexit
|
|
||||||
```
|
|
||||||
大多数的Linux系统,该参数的值都是off,当然也不排除个别情况或者人为修改
|
|
||||||
所以多数情况下我们可以用后台任务来作为守护进程
|
|
||||||
|
|
||||||
### disown 命令
|
|
||||||
该命令是将指定的一个后台任务移出后台任务列表
|
|
||||||
这样无论上面提到的参数设定是什么,它都不会再收到SIGNUP信号了
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
常见用法
|
|
||||||
```bash
|
|
||||||
# 移出最近一个正在执行的后台任务
|
|
||||||
disown
|
|
||||||
|
|
||||||
# 移出所有正在执行的后台任务
|
|
||||||
disown -r
|
|
||||||
|
|
||||||
# 移出所有后台任务
|
|
||||||
disown -a
|
|
||||||
|
|
||||||
# 不移出后台任务,但是让它们不会收到SIGHUP信号
|
|
||||||
disown -h
|
|
||||||
|
|
||||||
# 根据jobId,移出指定的后台任务
|
|
||||||
disown %2
|
|
||||||
disown -h %2
|
|
||||||
```
|
|
||||||
上图中的`[1]`就是jobId
|
|
||||||
|
|
||||||
当然有些程序执行会有大量的标准输出
|
|
||||||
习惯上我们会将需要后台运行的进程标准输出、错误输出、标准输入进行重定向
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node server.js > stdout.txt 2 > stderr.txt < /dev/null &
|
|
||||||
disown
|
|
||||||
```
|
|
||||||
|
|
||||||
### nohup 命令
|
|
||||||
实现的目标是一样的,这个命令还更加简单一些
|
|
||||||
```
|
|
||||||
nohup node server.js &
|
|
||||||
```
|
|
||||||
`nohup`做了3件事
|
|
||||||
+ 阻止SIGHUP信号发到这个进程。
|
|
||||||
+ 关闭标准输入。该进程不再能够接收任何输入,即使运行在前台。
|
|
||||||
+ 重定向标准输出和标准错误到文件nohup.out。
|
|
||||||
|
|
||||||
该命令实际上是把子进程与它所在的session分离了
|
|
||||||
但是它并不会自动把进程转换为后台任务,所以`&`还是要加的
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 任务、进程与线程
|
|
||||||
`任务(task)`本身是一个逻辑概念,是指由软件完成的一个活动,或者说是一系列共同达到某一目的的操作
|
|
||||||
它可以是一个或多个进程,也可以是线程
|
|
||||||
|
|
||||||
在说进程之前,必须要先说一下`程序(program)`,它是一段静态的代码,是保存在非易失性存储器上的的**指令和数据的有序集合**,没有任何执行的概念
|
|
||||||
|
|
||||||
`进程(process)`是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程,它是操作系统进行资源分配和调度的基本单元。
|
|
||||||
它是程序的一次执行过程,包括了动态**创建、调度、执行和消亡**的整个过程(这一系列都由操作系统的调度机制来完成),它是程序执行和资源管理的最小单位。进程不但包括程序的指令和数据,而且包括程序计数器和处理器的所有寄存器以及存储临时数据的进程堆栈。从操作系统的角度看,进程是程序执行时相关资源的总称。当进程结束时,所有资源被操作系统自动回收。
|
|
||||||
|
|
||||||
> 一次任务的运行可以同时激活多个进程,这些进程相互合作来完成该任务的一个最终目标。
|
|
||||||
|
|
||||||
#### 线程
|
|
||||||
进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段、代码段和堆栈段,这就造成了进程在进行切换时操作系统的开销比较大。
|
|
||||||
为了提高效率,操作系统又引入了另一个概念——`线程(thread)`。
|
|
||||||
线程是进程上下文中执行的代码序列,又称为轻量级的进程。它是操作系统能够调度的最小单元。线程可以对进程的内存空间和资源进行访问,并与同一进程中的其他线程共享。因此,线程的上下文切换的开销比进程小得多。一个进程可以拥有多个线程,其中每个线程共享该进程所拥有的资源。
|
|
||||||
要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响。由此可知,多线程中的同步是非常重要的问题
|
|
||||||
@ -1,161 +0,0 @@
|
|||||||
---
|
|
||||||
title: xargs命令
|
|
||||||
date: 2019-8-23 23:05:33
|
|
||||||
tags:
|
|
||||||
- linux
|
|
||||||
categories:
|
|
||||||
- linux
|
|
||||||
---
|
|
||||||
|
|
||||||
`xargs`命令的作用可以概括为将标准输入转化为命令行参数
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
### 标准输入与管道符
|
|
||||||
|
|
||||||
Linux下有些命令是可以接受标准输入的
|
|
||||||
例如下面这些命令
|
|
||||||
+ `cat` 查看文件内容
|
|
||||||
+ `sort` 对文件内容的行进行排序
|
|
||||||
+ `uniq` 报告或忽略文件中重复的行,必须先进行排序,所以常与sort配合使用
|
|
||||||
+ `grep` 根据规则找出匹配到的行
|
|
||||||
+ `wc` 获得文件中换行符,字,和字节个数
|
|
||||||
+ `head` 输出文件的开头部分
|
|
||||||
+ `tail` 输出文件的结尾部分
|
|
||||||
+ `tee` 从标准输入读取数据,并同时写到标准输出和文件
|
|
||||||
|
|
||||||
#### 标准输入的重定向
|
|
||||||
我们以sort命令为例
|
|
||||||
所有可以接受标准输入的默认标准输入指向都是**控制台输入**
|
|
||||||
如果直接执行sort
|
|
||||||

|
|
||||||
如果我们把这些内容写到**1.txt**这个文件当中
|
|
||||||
就可以执行
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sort 1.txt
|
|
||||||
# 或者
|
|
||||||
sort < 1.txt
|
|
||||||
```
|
|
||||||
需要注意的是这两种写法虽然效果一样,但是含义是不同的
|
|
||||||
前者是给sort命令的传参,后者则是输入重定向
|
|
||||||
|
|
||||||
查看sort命令的帮助可以看到相关说明
|
|
||||||

|
|
||||||
> 如果没有指定文件,则从标准输入读取
|
|
||||||
|
|
||||||
所以前者并没有改变标准输入的指向,只是因为指定了文件,该命令本身不再从标准输入读取
|
|
||||||
后者并没有指定文件,而是改变了标准输入的指向
|
|
||||||
|
|
||||||
#### 管道符
|
|
||||||
Linux当中的`|`称为管道符,使用方式为
|
|
||||||
```
|
|
||||||
command1 | command2
|
|
||||||
```
|
|
||||||
它的作用是把command1的标准输出作为command2的标准输入,相当于对command2做了标准输入的重定向
|
|
||||||
可以利用它完成较为复杂的链式处理
|
|
||||||
比如
|
|
||||||
```bash
|
|
||||||
tail < 1.log | sort | uniq -d -c | grep 'ab'
|
|
||||||
```
|
|
||||||
表示的含义是
|
|
||||||
1. 读取1.log文件的最后10行
|
|
||||||
2. 将这10行的内容作为sort的标准输入
|
|
||||||
3. 将排序后的内容作为uniq的标准输入,找到其中重复出现的行,以及出现的次数
|
|
||||||
4. 重复出现的行内容作为grep的标准输入,从中筛选包含字符串ab行
|
|
||||||
|
|
||||||
|
|
||||||
### xargs命令
|
|
||||||
使用管道符进行链式处理非常方便,但是能够接受标准输入的命令其实很少
|
|
||||||
大多数命令都不接受,只能接收命令行的传参
|
|
||||||
比如常见的`ls`命令,用来查看目录下的文件,它就不接受标准输入
|
|
||||||
例如现在有**apache2**和**php-apache**这两个目录,我们要列出这两个目录里面各自包含的文件
|
|
||||||
先创建好一个input.txt文件,内容就是
|
|
||||||
```
|
|
||||||
apache2 php-apache
|
|
||||||
```
|
|
||||||
命令写法
|
|
||||||
```bash
|
|
||||||
# 当然可以这样做,常规的传参方式
|
|
||||||
ls apache2 php-apache
|
|
||||||
|
|
||||||
# 如果要从其他来源读取要列出内容的目录呢
|
|
||||||
ls < input.txt #这样是无效的
|
|
||||||
cat input.txt | ls #这样与上面的是在做一样的事,同样无效
|
|
||||||
|
|
||||||
# 必须使用xargs把标准输出转化为传参
|
|
||||||
cat input.txt | xargs ls
|
|
||||||
```
|
|
||||||
管道符把标准输出作为xargs的标准输入
|
|
||||||
xargs则把标准输入转化为了命令行参数,传递给后面真正要执行的命令,在这里就是ls
|
|
||||||
|
|
||||||
> 如果xargs后面没有跟任何命令,那么就代表跟的是echo
|
|
||||||
echo命令的作用就是把接收到的参数输出到标准输出
|
|
||||||
|
|
||||||
#### 单独使用
|
|
||||||
xargs虽然有这些作用,但是它本身仍然是个可以接受标准输入的命令
|
|
||||||
如果没有进行标准输入的重定向 (`<`或者`|`)
|
|
||||||
那么它的标准输入就是指向控制台,后面相当于跟着echo
|
|
||||||
单独执行xargs的表现也就可以理解了
|
|
||||||
```bash
|
|
||||||
root@ubuntu:~$ xargs
|
|
||||||
abcd #Ctrl+D
|
|
||||||
abcd
|
|
||||||
```
|
|
||||||
命令行会等待用户输入,并且把这些输入直接输出
|
|
||||||
|
|
||||||
如果跟上了其他命令呢
|
|
||||||
```bash
|
|
||||||
root@ubuntu:~$ xargs find -name
|
|
||||||
*.txt #Ctrl+D
|
|
||||||
./input.txt
|
|
||||||
./1.txt
|
|
||||||
```
|
|
||||||
执行后会等待用户输入,Ctrl+D结束输入
|
|
||||||
把这些输入的内容作为参数提供给find命令
|
|
||||||
所以相当于执行`find -name *.txt`
|
|
||||||
|
|
||||||
### 其他使用技巧
|
|
||||||
#### 与find的搭配使用
|
|
||||||
|
|
||||||
如果我们要删除7天之前修改的文件
|
|
||||||
由于rm命令只能按照文件名匹配,find则具备多种方式的文件查找
|
|
||||||
所以两者搭配使用更加方便
|
|
||||||
|
|
||||||
|
|
||||||
先熟悉一下find命令如何按时间查找文件
|
|
||||||
|
|
||||||
|+n|n|-n|
|
|
||||||
|--|-|--|
|
|
||||||
|(n+1)\*24H前|(n+1)\*24H ~ n\*24H之间|n*24H以内|
|
|
||||||
|
|
||||||
+ `[a|c|m]min` \[最后访问|最后状态修改|最后内容修改\]min(单位是分钟)
|
|
||||||
+ `[a|c|m]time` \[最后访问|最后状态修改|最后内容修改\]time(单位是天)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查找当前目录7天之前修改的文件,并且作为rm的参数执行删除
|
|
||||||
find ./ -mtime +6 | xargs rm
|
|
||||||
```
|
|
||||||
上面的写法有个问题,就是xargs是使用**空白**作为参数的分隔的,但是文件名可能包含空格
|
|
||||||
应该使用下面的写法
|
|
||||||
```bash
|
|
||||||
find ./ -mtime +6 -print0 | xargs -0 rm
|
|
||||||
```
|
|
||||||
`-print0`提供给find命令,代表输出的文件列表以null作为分隔
|
|
||||||
`-0`提供给xargs命令,代表以null作为分隔来切分参数列表
|
|
||||||
|
|
||||||
#### -p 和 -t 参数
|
|
||||||
过长的命令串联有种难以掌控的感觉,如果出现了失误可能造成无法挽回的结果
|
|
||||||
`-t`参数可以在执行xargs后面命令之前打印出要执行的内容
|
|
||||||
`-p`参数会等待用户确认,在用户输入y(大小写皆可)后再执行
|
|
||||||
|
|
||||||
|
|
||||||
#### -L 参数
|
|
||||||
xargs把标准输入转化为参数提供给后面的命令执行,默认情况下是执行一次的
|
|
||||||
如果我们要把每一行的输出循环执行呢
|
|
||||||
这就需要用到`-L`参数了,后面跟上行数,代表把几行作为一组参数传递给后面的命令执行一次
|
|
||||||

|
|
||||||
|
|
||||||
<del>要问带\n的字符串为啥这样写,我只能说shell的晦涩之处不止于此</del>
|
|
||||||
可以看到前一次执行了`ls -l -h`
|
|
||||||
后一次分别执行了`ls -l`和`ls -h`
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
---
|
|
||||||
title: parcel+react+tsx
|
|
||||||
date: 2021-4-6 12:01:13
|
|
||||||
tags:
|
|
||||||
- react
|
|
||||||
categories:
|
|
||||||
- react
|
|
||||||
---
|
|
||||||
|
|
||||||
熟悉一下react,使用`parcel`作为打包工具,使用`ant-design`作为组件库
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
### 添加依赖
|
|
||||||
```json
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^17.0.3",
|
|
||||||
"parcel-bundler": "^1.12.5",
|
|
||||||
"typescript": "^4.2.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tensorflow/tfjs": "^3.3.0",
|
|
||||||
"antd": "^4.15.0",
|
|
||||||
"dayjs": "^1.10.4",
|
|
||||||
"react": "^17.0.2",
|
|
||||||
"react-dom": "^17.0.2"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
其中`react`和`react-dom`是使用react构建项目必须的
|
|
||||||
如果需要使用typescript编写代码,还需要引入`@types/react`
|
|
||||||
另外dayjs是antd要求引入的依赖
|
|
||||||
|
|
||||||
### 模板页面
|
|
||||||
```html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>测试页面</title>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script src="./index.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
添加一个id为app的div作为根元素
|
|
||||||
并且引入入口文件index.tsx
|
|
||||||
|
|
||||||
### tsconfig.json配置文件
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react",
|
|
||||||
"esModuleInterop": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 编写根组件
|
|
||||||
```tsx
|
|
||||||
// App.tsx
|
|
||||||
import React, { PureComponent }from 'react'
|
|
||||||
import { Button, DatePicker } from 'antd'
|
|
||||||
import 'antd/dist/antd.css'
|
|
||||||
|
|
||||||
class App extends PureComponent {
|
|
||||||
private model: tf.LayersModel;
|
|
||||||
async componentDidMount() {
|
|
||||||
console.log('mount生命周期')
|
|
||||||
}
|
|
||||||
render() {
|
|
||||||
return ( // 使用ant-design的组件
|
|
||||||
<div>
|
|
||||||
<Button type="primary">我是一个按钮</Button>
|
|
||||||
<DatePicker onChange={this.onChange} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onChange(date, dateString: string) {
|
|
||||||
console.log(date, dateString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
```
|
|
||||||
|
|
||||||
### 编写入口文件
|
|
||||||
```tsx
|
|
||||||
// index.tsx
|
|
||||||
import React from 'react'
|
|
||||||
import ReactDom from 'react-dom'
|
|
||||||
import App from './App'
|
|
||||||
|
|
||||||
ReactDom.render(<App/>, document.querySelector('#app'))
|
|
||||||
```
|
|
||||||
这里的操作就是将上一步编写的根组件加载到根元素当中
|
|
||||||
|
|
||||||
### 开发模式与打包
|
|
||||||
开发模式
|
|
||||||
```
|
|
||||||
parcel src/index.html
|
|
||||||
```
|
|
||||||
打包构建
|
|
||||||
```
|
|
||||||
parcel build src/index.html
|
|
||||||
```
|
|
||||||
|
|
||||||
### react组件生命周期
|
|
||||||
|
|
||||||
组件的生命周期可分成三个状态:
|
|
||||||
+ **Mounting**:已插入真实 DOM
|
|
||||||
+ **Updating**:正在被重新渲染
|
|
||||||
+ **Unmounting**:已移出真实 DOM
|
|
||||||
|
|
||||||
生命周期的方法
|
|
||||||
|
|
||||||
+ **componentWillMount** 在渲染前调用,在客户端也在服务端。
|
|
||||||
+ **componentDidMount** 在第一次渲染后调用,只在客户端。之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。
|
|
||||||
+ **componentWillReceiveProps** 在组件接收到一个新的 prop (更新后)时被调用。这个方法在初始化render时不会被调用。
|
|
||||||
+ **shouldComponentUpdate** 返回一个布尔值。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。
|
|
||||||
可以在你确认不需要更新组件时使用。
|
|
||||||
+ **componentWillUpdate** 在组件接收到新的props或者state但还没有render时被调用。在初始化时不会被调用。
|
|
||||||
+ **componentDidUpdate** 在组件完成更新后立即调用。在初始化时不会被调用。
|
|
||||||
+ **componentWillUnmount** 在组件从 DOM 中移除之前立刻被调用。
|
|
||||||
|
|
||||||
### 路由
|
|
||||||
引入依赖
|
|
||||||
```
|
|
||||||
npm install react-router-dom --save
|
|
||||||
npm install @types/react-router-dom --save-dev
|
|
||||||
```
|
|
||||||
> `react-router`与`react-router-dom`
|
|
||||||
> 前者实现了路由的核心功能
|
|
||||||
> 后者基于react-router,加入了在浏览器运行环境下的一些功能
|
|
||||||
> 引入后者相当于间接引入了前者
|
|
||||||
|
|
||||||
编写路由页面
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import React, { PureComponent } from 'react'
|
|
||||||
import { NavLink, Route, HashRouter } from 'react-router-dom'
|
|
||||||
|
|
||||||
class APage extends PureComponent {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>我是A页面</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class BPage extends PureComponent {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>我是B页面</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class App extends PureComponent {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<HashRouter>
|
|
||||||
<div>
|
|
||||||
<span>这里是主页面</span>
|
|
||||||
<ul>
|
|
||||||
<li><NavLink to='/a' activeClassName="active">A页面</NavLink></li>
|
|
||||||
<li><NavLink to='/b' activeClassName="active">B页面</NavLink></li>
|
|
||||||
</ul>
|
|
||||||
<Route path='/a' component={APage}></Route>
|
|
||||||
<Route path='/b' component={BPage}></Route>
|
|
||||||
</div>
|
|
||||||
</HashRouter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
```
|
|
||||||
|
|
||||||
+ 路由分为`HashRouter`和`BrowserRouter`两种
|
|
||||||
相当于vue-router当中的**hash**和**history**两种模式
|
|
||||||
+ `NavLink`会被渲染为一个a标签,作为路由跳转的链接
|
|
||||||
与`Link`相比多了一些属性,比如**activeClassName**,便于样式的调整
|
|
||||||
+ `Route`就是路由对应的页面
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
---
|
|
||||||
title: CSS属性shape-outside
|
|
||||||
date: 2020-7-4 10:47:49
|
|
||||||
tags:
|
|
||||||
- 前端
|
|
||||||
- css
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
`shape-outside`的CSS属性定义了一个可以是非矩形的形状,相邻的内联内容应围绕该形状进行包装。默认情况下,内联内容包围其边距框
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
### 基本用法
|
|
||||||
#### 关键字值
|
|
||||||
+ margin-box
|
|
||||||
+ content-box
|
|
||||||
+ border-box
|
|
||||||
+ padding-box
|
|
||||||
|
|
||||||
这几个关键字值就是表示该形状的边界是盒子模型的哪一层
|
|
||||||
如果使用`box-sizing`属性也可以达到相同的目的
|
|
||||||
|
|
||||||
#### 图形函数值
|
|
||||||
可以使用**图形函数**
|
|
||||||
+ `circle([<shape-radius>]? [at <position>]?)` - 圆形,这个函数接受两个参数,均为可选
|
|
||||||
第一个参数表示圆的半径(负值无效),第二个参数表示圆心的定位(注意圆心定位前需要加**at**关键字)
|
|
||||||
```css
|
|
||||||
.container {
|
|
||||||
background: #ccc;
|
|
||||||
overflow: auto;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.container > .inner {
|
|
||||||
background-color: white;
|
|
||||||
height: 100px;
|
|
||||||
width: 100px;
|
|
||||||
float: left;
|
|
||||||
shape-outside: circle(50% at 20px 30px);
|
|
||||||
}
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
+ `ellipse([<shape-radius>{2}]? [at <position>]?)` - 椭圆形
|
|
||||||
这个函数与circle非常类似,差别就是shape-radius如果指定,需要指定两个值,分别表示X轴方向的半径和Y轴方向的半径
|
|
||||||
|
|
||||||
+ `inset(<shape-arg>{1,4} [round <border-radius>]?)` - 矩形
|
|
||||||
前四个参数分别代表了插进的长方形与相关盒模型的上,右,下与左边界和顶点的偏移量。这些参数遵循边际速记语法(the syntax of the margin shorthand),所以给予一个、两个、或四个值都能设置四个偏移量。
|
|
||||||
round后跟随的值表示圆角弧度
|
|
||||||
> 边际速记语法:1个值表示上下左右;2个值分别表示上下、左右;3个值表示上、左右、下,4个值表示上、右、下、左。
|
|
||||||
比如margin和padding的值使用的都是这种规则
|
|
||||||
|
|
||||||
+ `polygon([<fill-rule>,]? [<shape-arg> <shape-arg>]#)` - 多边形
|
|
||||||
fill-rule有`nonzero | evenodd`两个值,用于指定确定点在内部和外部的规则,每一对sharp-arg的值代表一个多边形顶点坐标
|
|
||||||
例如
|
|
||||||
```css
|
|
||||||
.container > .inner {
|
|
||||||
/* 省略其他无关属性 */
|
|
||||||
shape-outside: polygon(10px 10px, 20px 20px, 30px 30px);
|
|
||||||
}
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
#### 使用图片形状
|
|
||||||
通常使用PNG图片,因为这种图片允许透明像素,如果是JPG这种只能是矩形的,也就没有什么意义了
|
|
||||||
根据图片的非透明区域进行包裹
|
|
||||||
```css
|
|
||||||
.container > .inner {
|
|
||||||
shape-outside: url(/example/demo.png);
|
|
||||||
}
|
|
||||||
```
|
|
||||||

|
|
||||||
实际使用的是该图片的轮廓,这个属性并不会使图片显示出来,如果要看到图片,还需要配合background属性
|
|
||||||
|
|
||||||
|
|
||||||
### 用于盒子内部
|
|
||||||
该属性是用于指定当前元素外部的形状,如果要在内部使用,就需要进行一些变通
|
|
||||||
在内部添加DOM结构进行占位,可以使用实体DOM,也可以使用伪元素
|
|
||||||
```html
|
|
||||||
<div class="circle">
|
|
||||||
<div class="before"></div>
|
|
||||||
<div class="after"></div>
|
|
||||||
这里是内部的文字...
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
```css
|
|
||||||
.circle {
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 207px;
|
|
||||||
height: 250px;
|
|
||||||
color: white;
|
|
||||||
background-color: deepskyblue;
|
|
||||||
padding: 10px;
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
.before {
|
|
||||||
float: left;
|
|
||||||
width: 50%; height: 100%;
|
|
||||||
shape-outside: radial-gradient(farthest-side ellipse at right, transparent 100%, red);
|
|
||||||
}
|
|
||||||
.after {
|
|
||||||
float: right;
|
|
||||||
width: 50%; height: 100%;
|
|
||||||
shape-outside: radial-gradient(farthest-side ellipse at left, transparent 100%, red);
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||

|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
---
|
|
||||||
title: CSS计数器
|
|
||||||
date: 2022-12-30 14:53:47
|
|
||||||
tags:
|
|
||||||
- 前端
|
|
||||||
- css
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
### 有序列表
|
|
||||||
HTML具备原生支持的计数器功能,也就是有序列表`<ol>`
|
|
||||||
```html
|
|
||||||
<ol class="normal">
|
|
||||||
<li>aaa</li>
|
|
||||||
<li>aaa</li>
|
|
||||||
<li>AAA
|
|
||||||
<ol class="roman"><li>bbb</li><li>bbb</li></ol>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
```
|
|
||||||
<!-- more -->
|
|
||||||
对于有序列表来说,可以去指定`list-style-type`来使用预置的序列,比如字母、罗马数字等,默认是**decimal**(数字)
|
|
||||||
也可以给`::marker`伪元素指定样式
|
|
||||||
```less
|
|
||||||
ol.roman {
|
|
||||||
list-style-type: lower-roman;
|
|
||||||
li::marker {
|
|
||||||
color: red;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
实际效果如下
|
|
||||||

|
|
||||||
|
|
||||||
这样的有序列表有一定的局限性
|
|
||||||
1. 不能指定起始值,每个`<ol>`当中的`<li>`都是从1开始的
|
|
||||||
2. 不能指定步长,一定是每次递增1
|
|
||||||
|
|
||||||
### CSS计数器
|
|
||||||
如果需要对序列的控制更灵活一些,可以使用`CSS计数器`
|
|
||||||
```html
|
|
||||||
<div class="container">
|
|
||||||
<div class="counter">第一章</div>
|
|
||||||
<div class="counter">第二章</div>
|
|
||||||
<div class="counter">第三章</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
```less
|
|
||||||
.container {
|
|
||||||
counter-reset: counter1 0;
|
|
||||||
> .counter::before {
|
|
||||||
counter-increment: counter1 1;
|
|
||||||
content: counter(counter1,decimal) '. ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
效果如下
|
|
||||||

|
|
||||||
|
|
||||||
1. `counter-reset`代表在这个层级重置指定计数器的值,默认置为0。可以指定多个例如**counter-reset: counter1 0 counter2 10**
|
|
||||||
无论在何处,只要有它,对应的计数器的值都会**重置**,所以通常会将它给到列表的**父元素**
|
|
||||||
2. `counter-increment`代表计数器增加,默认增加1。与counter-reset类似也可以指定多个
|
|
||||||
无论在何处,只要有它,对应的计数器的值都会**增加**,所以通常会将它给到列表的**子元素**
|
|
||||||
3. `counter`函数指定使用该计数器,可以用于伪元素的content,它相当于是执行输出
|
|
||||||
这个函数可以有2个参数`counter(name,style)`,style就是跟**list-style-type**一样,可以指定不同的序号风格
|
|
||||||
|
|
||||||
#### 嵌套计数
|
|
||||||
实际使用当中我们可以用计数器生成目录树结构
|
|
||||||
可能需要有多个层级
|
|
||||||
但是如果层级数量是不确定的,基本的计数器就无法满足了,这种情况下就需要用到`嵌套计数`
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="reset">
|
|
||||||
<div class="counters">第一章
|
|
||||||
<div class="reset">
|
|
||||||
<div class="counters">第1节</div>
|
|
||||||
<div class="counters">第2节
|
|
||||||
<div class="reset">
|
|
||||||
<div class="counters">第1小节</div>
|
|
||||||
<div class="counters">第2小节</div>
|
|
||||||
<div class="counters">第3小节</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="counters">第3节</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="counters">第二章</div>
|
|
||||||
<div class="counters">第三章
|
|
||||||
<div class="reset">
|
|
||||||
<div class="counters">第1节</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
```less
|
|
||||||
.reset {
|
|
||||||
padding-left: 20px;
|
|
||||||
counter-reset: counter2;
|
|
||||||
.counters::before {
|
|
||||||
counter-increment: counter2;
|
|
||||||
content: counters(counter2, '-') '. ';
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
效果如下
|
|
||||||

|
|
||||||
与基本计数器的区别就在于使用`counters`函数,形式为`counters(name, string, style)`
|
|
||||||
name和style与基本计数器中的含义相同,**string**为拼接使用的字符串
|
|
||||||
|
|
||||||
#### 其他特性
|
|
||||||
如果其中存在使用`display:none`隐藏掉元素,则计数器的值不会增加
|
|
||||||
实际显示的序列仍然是正确的
|
|
||||||
如果使用`visibility:hidden`隐藏,计数器的值会增加
|
|
||||||
这一点与有序列表是一样的
|
|
||||||
@ -17,8 +17,8 @@ npm install hexo-deployer-git --save-dev
|
|||||||
```
|
```
|
||||||
#### 创建github仓库
|
#### 创建github仓库
|
||||||
仓库的名字必须是`[github用户名].github.io`
|
仓库的名字必须是`[github用户名].github.io`
|
||||||
比如我的github用户名是colorfulsweet
|
比如我的github用户名是sookie2010
|
||||||
那么就应该创建名为 **colorfulsweet.github.io** 的仓库
|
那么就应该创建名为 **sookie2010.github.io** 的仓库
|
||||||
|
|
||||||
#### 添加配置项
|
#### 添加配置项
|
||||||
在`_config.yml`当中配置
|
在`_config.yml`当中配置
|
||||||
@ -28,8 +28,8 @@ deploy:
|
|||||||
repo: git@github.com:[github用户名]/[github用户名].github.io.git
|
repo: git@github.com:[github用户名]/[github用户名].github.io.git
|
||||||
branch: master
|
branch: master
|
||||||
```
|
```
|
||||||
比如我的github用户名是colorfulsweet
|
比如我的github用户名是sookie2010
|
||||||
那么repo就是 ` git@github.com:colorfulsweet/colorfulsweet.github.io.git`
|
那么repo就是 ` git@github.com:sookie2010/sookie2010.github.io.git`
|
||||||
|
|
||||||
#### 静态化与发布
|
#### 静态化与发布
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -30,7 +30,9 @@ search:
|
|||||||
<link href="/文章URL地址/"/>
|
<link href="/文章URL地址/"/>
|
||||||
<url>/文章URL地址/</url>
|
<url>/文章URL地址/</url>
|
||||||
<content type="html">
|
<content type="html">
|
||||||
|
<![CDATA[
|
||||||
<p>这里是文章内容</p>
|
<p>这里是文章内容</p>
|
||||||
|
]]>
|
||||||
</content>
|
</content>
|
||||||
<categories>
|
<categories>
|
||||||
<category>分类1</category>
|
<category>分类1</category>
|
||||||
|
|||||||
@ -1,185 +0,0 @@
|
|||||||
---
|
|
||||||
title: Three.js初见(1)
|
|
||||||
date: 2022-04-03 10:54:35
|
|
||||||
tags:
|
|
||||||
- 前端
|
|
||||||
- Three.js
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
Three.js是基于原生WebGL封装运行的三维引擎,在所有WebGL引擎中,Three.js是国内文资料最多、使用最广泛的三维引擎。
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
## 页面结构
|
|
||||||
```html
|
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>Three.js DEMO</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; }
|
|
||||||
canvas { width: 100%; height: 100% }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script src="./js/lib/three.js"></script>
|
|
||||||
<script src="./js/index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
在脚手架项目当中
|
|
||||||
也可以使用`npm install three`来添加依赖
|
|
||||||
|
|
||||||
## Hello World
|
|
||||||
index.js
|
|
||||||
```javascript
|
|
||||||
(function(){
|
|
||||||
// 创建场景
|
|
||||||
const scene = new THREE.Scene()
|
|
||||||
|
|
||||||
// 创建相机
|
|
||||||
// 1.视野角度 2.长宽比 3.远剪切面 4.近剪切面
|
|
||||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
|
||||||
|
|
||||||
// 创建WebGL渲染器
|
|
||||||
const renderer = new THREE.WebGLRenderer()
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
|
||||||
// 将canvas添加到页面当中
|
|
||||||
document.body.appendChild(renderer.domElement)
|
|
||||||
|
|
||||||
// 创建一个立方体
|
|
||||||
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
|
||||||
// 创建材质
|
|
||||||
const material = new THREE.MeshBasicMaterial({ color: 0x92d667 })
|
|
||||||
// 创建网格, 可以将材质和立方体放入其中
|
|
||||||
// 网格可以直接放入场景中, 并让它在场景中自由移动
|
|
||||||
const cube = new THREE.Mesh(geometry, material)
|
|
||||||
scene.add(cube)
|
|
||||||
|
|
||||||
// 将摄像机向外移动一些 防止与物体重叠
|
|
||||||
camera.position.z = 5
|
|
||||||
|
|
||||||
// 使用渲染器 渲染到场景当中
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
})()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
> 添加到场景中的几何体默认位于场景的坐标原点(0, 0, 0)
|
|
||||||
|
|
||||||
### 添加多个几何体
|
|
||||||
```javascript
|
|
||||||
// 圆柱网格模型
|
|
||||||
const geometry2 = new THREE.CylinderGeometry( 8, 8, 8, 100 )
|
|
||||||
const material2 = new THREE.MeshBasicMaterial({ color: 0x92d667 })
|
|
||||||
const mesh2 = new THREE.Mesh(geometry2, material2)
|
|
||||||
mesh2.translateY(20) // 向Y轴正方向移动20
|
|
||||||
scene.add(mesh2)
|
|
||||||
```
|
|
||||||
为防止几何体重叠无法看到, 可以调整网格的位置
|
|
||||||
|
|
||||||
### 循环渲染
|
|
||||||
上面的代码, 已经可以显示出一个立方体
|
|
||||||
如果要让它有一个动画效果
|
|
||||||
需要改变网格的旋转
|
|
||||||
并且进行循环渲染
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 循环渲染
|
|
||||||
function animate() {
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
cube.rotation.x += 0.01
|
|
||||||
cube.rotation.y += 0.01
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
}
|
|
||||||
animate()
|
|
||||||
```
|
|
||||||
`requestAnimationFrame`主要的好处是当前页面隐藏时不会进行渲染
|
|
||||||
如果是使用定时器
|
|
||||||
那么它在后台也会一直执行, 占用的资源比较多
|
|
||||||
其次是它执行的频率由屏幕刷新率决定, 效果可以更加流畅
|
|
||||||
|
|
||||||
|
|
||||||
## 场景控制
|
|
||||||
要使用鼠标或键盘控制三维场景, 可以使用`OrbitControls`控件
|
|
||||||
首先引入它
|
|
||||||
```html
|
|
||||||
<script src="./js/lib/OrbitControls.js"></script>
|
|
||||||
```
|
|
||||||
调用方式
|
|
||||||
```javascript
|
|
||||||
// 创建控件对象
|
|
||||||
const controls = new THREE.OrbitControls(camera, renderer.domElement)
|
|
||||||
// 监听鼠标、键盘事件
|
|
||||||
controls.addEventListener('change', function(){
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
场景操作
|
|
||||||
|
|
||||||
+ 缩放:滚动—鼠标中键
|
|
||||||
+ 旋转:拖动—鼠标左键
|
|
||||||
+ 平移:拖动—鼠标右键
|
|
||||||
|
|
||||||
> 如果代码中通过`requestAnimationFrame`实现渲染器渲染方法`render`的周期性调用,当通过OrbitControls操作改变相机状态的时候,没必要在通过`controls.addEventListener('change', render)`监听鼠标事件调用渲染函数,因为`requestAnimationFrame`就会不停的调用渲染函数。
|
|
||||||
|
|
||||||
## 辅助坐标系
|
|
||||||
|
|
||||||
为了方便调试预览threejs提供了一个辅助三维坐标系`AxesHelper`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 辅助坐标系 参数代表轴的线段长度,可以根据场景大小去设置
|
|
||||||
const axesHelper = new THREE.AxesHelper(100);
|
|
||||||
scene.add(axesHelper);
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
> 红色代表 X 轴, 绿色代表 Y 轴, 蓝色代表 Z 轴
|
|
||||||
|
|
||||||
## 材质与光源
|
|
||||||
除了基本的材质类型之外, three还提供了多种材质效果
|
|
||||||
|
|
||||||
| 材质类型 | 功能 |
|
|
||||||
| -- | -- |
|
|
||||||
| MeshBasicMaterial | 基础网格材质,不受光照影响的材质 |
|
|
||||||
| MeshLambertMaterial | Lambert网格材质,与光照有反应,漫反射 |
|
|
||||||
| MeshPhongMaterial | 高光Phong材质,与光照有反应 |
|
|
||||||
| MeshStandardMaterial | PBR物理材质,相比较高光Phong材质可以更好的模拟金属、玻璃等效果 |
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 创建一个球体
|
|
||||||
const geometry1 = new THREE.SphereGeometry(6, 40, 40)
|
|
||||||
const material1 = new THREE.MeshPhongMaterial({
|
|
||||||
color: 0x0000ff,
|
|
||||||
specular: 0x4488ee, // 高光颜色
|
|
||||||
shininess: 12, // 高亮程度
|
|
||||||
})
|
|
||||||
const mesh1 = new THREE.Mesh(geometry1, material1)
|
|
||||||
scene.add(mesh1)
|
|
||||||
|
|
||||||
// 创建一个圆柱
|
|
||||||
const geometry2 = new THREE.CylinderGeometry( 8, 8, 8, 100 )
|
|
||||||
const material2 = new THREE.MeshLambertMaterial({
|
|
||||||
color: 0xffffff,
|
|
||||||
})
|
|
||||||
const mesh2 = new THREE.Mesh(geometry2, material2)
|
|
||||||
mesh2.translateY(20) // 向Y轴正方向移动20
|
|
||||||
scene.add(mesh2)
|
|
||||||
```
|
|
||||||
|
|
||||||
`MeshLambertMaterial`和`MeshPhongMaterial`都是对光照有反应的
|
|
||||||
但是此时我们还需要添加一个光源, 才能看到效果
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 点光源
|
|
||||||
const pointLight = new THREE.PointLight(0xffffff)
|
|
||||||
pointLight.position.set(100, 100, 0) // 点光源位置
|
|
||||||
scene.add(pointLight) // 点光源添加到场景中
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
---
|
|
||||||
title: Three.js初见(2)
|
|
||||||
date: 2022-04-10 22:02:34
|
|
||||||
tags:
|
|
||||||
- 前端
|
|
||||||
- Three.js
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
## 自定义几何体
|
|
||||||
|
|
||||||
Three.js当中的几何体都是由若干个三角形构成的
|
|
||||||
哪怕是球形、圆柱形这些立体图形,也不存在真正的曲面,只不过这些三角形越是细小,拼凑起来越接近曲面的效果
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
基于这种方式,我们可以构造自定义的图形
|
|
||||||
需要使用类型化数组`TypedArray`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const geometry = new THREE.BufferGeometry(); //创建一个Buffer类型几何体对象
|
|
||||||
// 类型数组创建顶点数据
|
|
||||||
const vertices = new Uint8Array([
|
|
||||||
0, 0, 0, // 顶点1坐标
|
|
||||||
10, 0, 0, // 顶点2坐标
|
|
||||||
0, 20, 0, // 顶点3坐标
|
|
||||||
0, 0, 10, // 顶点4坐标
|
|
||||||
0, 0, 1, // 顶点5坐标
|
|
||||||
20, 0, 1, // 顶点6坐标
|
|
||||||
])
|
|
||||||
// 创建属性缓冲区对象
|
|
||||||
const attribue = new THREE.BufferAttribute(vertices, 3) // 3个为一组,表示一个顶点的xyz坐标
|
|
||||||
// 设置几何体的位置属性
|
|
||||||
geometry.setAttribute('position', attribue)
|
|
||||||
// 三角面(网格)渲染模式
|
|
||||||
const material = new THREE.MeshBasicMaterial({
|
|
||||||
color: 0x0000ff, // 三角面颜色
|
|
||||||
side: THREE.DoubleSide // 两面可见
|
|
||||||
})
|
|
||||||
const mesh = new THREE.Mesh(geometry, material)
|
|
||||||
scene.add(mesh)
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
上面的代码中使用的是`Uint8Array`, 实际上可以使用任何一种类型数组
|
|
||||||
|
|
||||||
### 点材质和线材质
|
|
||||||
上面的例子当中使用的是**网格材质**
|
|
||||||
threejs还提供了**点材质**和**线材质**
|
|
||||||
```javascript
|
|
||||||
// 点材质
|
|
||||||
const material = new THREE.PointsMaterial({
|
|
||||||
color: 0x0000ff,
|
|
||||||
size: 2
|
|
||||||
})
|
|
||||||
const points = new THREE.Points(geometry, material)
|
|
||||||
scene.add(points)
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 线材质
|
|
||||||
const material = new THREE.LineBasicMaterial({
|
|
||||||
color: 0x0000ff // 线条颜色
|
|
||||||
})
|
|
||||||
const line = new THREE.Line(geometry, material) // 线条模型对象
|
|
||||||
scene.add(line) // 线条对象添加到场景中
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
### 设置每个顶点的颜色
|
|
||||||
```javascript
|
|
||||||
const geometry = new THREE.BufferGeometry(); //创建一个Buffer类型几何体对象
|
|
||||||
// 类型数组创建顶点数据
|
|
||||||
const vertices = new Uint8Array([
|
|
||||||
0, 0, 0, // 顶点1坐标
|
|
||||||
10, 0, 0, // 顶点2坐标
|
|
||||||
0, 20, 0, // 顶点3坐标
|
|
||||||
0, 0, 10, // 顶点4坐标
|
|
||||||
0, 0, 1, // 顶点5坐标
|
|
||||||
20, 0, 1, // 顶点6坐标
|
|
||||||
])
|
|
||||||
// 创建属性缓冲区对象
|
|
||||||
const attribue = new THREE.BufferAttribute(vertices, 3) // 3个为一组,表示一个顶点的xyz坐标
|
|
||||||
// 设置几何体的位置属性
|
|
||||||
geometry.setAttribute('position', attribue)
|
|
||||||
|
|
||||||
// 点材质
|
|
||||||
const material = new THREE.PointsMaterial({
|
|
||||||
// color: 0x0000ff,
|
|
||||||
vertexColors: THREE.VertexColors, //以顶点颜色为准
|
|
||||||
size: 2
|
|
||||||
})
|
|
||||||
const pointColors = new Uint8Array([
|
|
||||||
1, 0, 0, //顶点1颜色
|
|
||||||
0, 1, 0, //顶点2颜色
|
|
||||||
0, 0, 1, //顶点3颜色
|
|
||||||
1, 1, 0, //顶点4颜色
|
|
||||||
0, 1, 1, //顶点5颜色
|
|
||||||
1, 0, 1, //顶点6颜色
|
|
||||||
])
|
|
||||||
geometry.attributes.color = new THREE.BufferAttribute(pointColors, 3) // 3个为一组代表一个顶点的RGB值
|
|
||||||
const points = new THREE.Points(geometry, material)
|
|
||||||
scene.add(points)
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
这个例子当中与上面的区别除了指定每个顶点的颜色之外
|
|
||||||
还需要给材质的`vertexColors`设置为`THREE.VertexColors`
|
|
||||||
该属性的默认值是`THREE.NoColors`, 意思就是模型的颜色取决于材质的color
|
|
||||||
|
|
||||||
如果把上述的方式使用到**三角面材质**上
|
|
||||||
可以得到渐变色的面
|
|
||||||

|
|
||||||
|
|
||||||
### 顶点坐标复用
|
|
||||||
由于threejs当中的图形都是由若干个三角形拼接而成
|
|
||||||
所以实际应用当中, 肯定是多个相邻三角形的顶点重合
|
|
||||||
如果每个三角形都要分别指定3个顶点的坐标, 数据肯定是非常冗余的
|
|
||||||
所以可以进行顶点坐标的复用
|
|
||||||
同时也不受限于坐标定义的顺序
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const geometry = new THREE.BufferGeometry(); //创建一个Buffer类型几何体对象
|
|
||||||
// 类型数组创建顶点数据
|
|
||||||
const vertices = new Int8Array([
|
|
||||||
0, 0, 0,
|
|
||||||
10, 0, 0,
|
|
||||||
10, 10, 0,
|
|
||||||
0, 10, 0,
|
|
||||||
-10, 10, 0,
|
|
||||||
-10, 0, 0,
|
|
||||||
])
|
|
||||||
// 创建属性缓冲区对象
|
|
||||||
const attribue = new THREE.BufferAttribute(vertices, 3) // 3个为一组,表示一个顶点的xyz坐标
|
|
||||||
// 设置几何体的位置属性
|
|
||||||
geometry.setAttribute('position', attribue)
|
|
||||||
// Uint16Array类型数组创建顶点索引数据
|
|
||||||
const indexes = new Uint8Array([
|
|
||||||
// 0对应第1个顶点
|
|
||||||
// 索引值3个为一组,表示一个三角形的3个顶点
|
|
||||||
0, 1, 2,
|
|
||||||
0, 2, 3,
|
|
||||||
0, 3, 4,
|
|
||||||
0, 4, 5,
|
|
||||||
])
|
|
||||||
// 索引数据赋值给几何体的index属性
|
|
||||||
geometry.index = new THREE.BufferAttribute(indexes, 1) //1个为一组
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
### Vector3和Color
|
|
||||||
|
|
||||||
除了使用`BufferGeometry`之外, 我们也可以使用`Vector3`和`Color`来创建自定义几何体
|
|
||||||
它们是基于`Geometry`这个API进行使用的
|
|
||||||
Threejs渲染的时候会先把 Geometry 转化为 BufferGeometry 再解析几何体顶点数据进行渲染
|
|
||||||
|
|
||||||
**Vector3**表示三维向量, 也可以理解为点的坐标
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const geometry = new THREE.Geometry()
|
|
||||||
// 类型数组创建顶点数据
|
|
||||||
geometry.vertices.push(
|
|
||||||
new THREE.Vector3(0, 0, 0),
|
|
||||||
new THREE.Vector3(10, 0, 0),
|
|
||||||
new THREE.Vector3(0, 20, 0),
|
|
||||||
new THREE.Vector3(0, 0, 10),
|
|
||||||
new THREE.Vector3(0, 0, 1),
|
|
||||||
new THREE.Vector3(20, 0, 1),
|
|
||||||
)
|
|
||||||
geometry.colors.push(
|
|
||||||
new THREE.Color(0xff0000),
|
|
||||||
new THREE.Color(0x00ff00),
|
|
||||||
new THREE.Color(0x0000ff),
|
|
||||||
new THREE.Color(0xffff00),
|
|
||||||
new THREE.Color(0x00ffff),
|
|
||||||
new THREE.Color(0xff00ff),
|
|
||||||
)
|
|
||||||
// 点材质
|
|
||||||
const material = new THREE.PointsMaterial({
|
|
||||||
vertexColors: THREE.VertexColors, //以顶点颜色为准
|
|
||||||
})
|
|
||||||
const points = new THREE.Points(geometry, material)
|
|
||||||
scene.add(points)
|
|
||||||
```
|
|
||||||
@ -1,207 +0,0 @@
|
|||||||
---
|
|
||||||
title: TypeScript初见
|
|
||||||
date: 2019-6-23 15:22:13
|
|
||||||
tags:
|
|
||||||
- TypeScript
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
`TypeScript` ( 以下简称TS ) 由微软开发, 是一个JavaScript的超集, 遵循ES6规范
|
|
||||||
也就是TS本身支持所有的JS语法, 已有的JS程序不需要任何改动就可以在TS环境下运行
|
|
||||||
同时进行了扩展
|
|
||||||
添加了基于类的面向对象编程特性, 以及数据类型
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
> 这种数据类型的定义, 与Java这种编译型语言终究不同
|
|
||||||
我的体会是它只存在编译期的类型检查, 而运行期还是与JS基本相同
|
|
||||||
这样的好处也很明显, 能够减少在开发阶段犯错误的几率 ( 毕竟IDE可以完成类型检查的错误提示 )
|
|
||||||
避免一些类型问题都需要调试来排查
|
|
||||||
|
|
||||||
### 开发环境
|
|
||||||
首先这种JavaScript的超集, 如果需要在浏览器运行
|
|
||||||
必然是需要compiler的, 因为需要转成可执行的JavaScript代码
|
|
||||||
|
|
||||||
如果是在nodejs里面运行, 就可以不需要了, 因为它可以想babel那样, 封装一个自己的运行器出来
|
|
||||||
比如babel的 `babel-node`, 就如同是nodejs的 `node`, 前者提供了提供一个支持ES6的REPL环境
|
|
||||||
|
|
||||||
```shell
|
|
||||||
npm install -g typescript
|
|
||||||
```
|
|
||||||
> 当然如果是只在项目里面使用, 也可以不执行全局安装
|
|
||||||
|
|
||||||

|
|
||||||
可以看到安装后添加了两个命令行工具, 分别是`tsc`和`tsserver`
|
|
||||||
tsc就是 TypeScript Compiler
|
|
||||||
|
|
||||||
#### 使用编译器
|
|
||||||
现在可以尝试写个typescript文件
|
|
||||||
比如
|
|
||||||
```typescript
|
|
||||||
// hello.ts
|
|
||||||
export class Hello {
|
|
||||||
hello(msg) {
|
|
||||||
console.log(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
然后执行`tsc hello.ts`
|
|
||||||
就会生成hello.js文件, 内容如下
|
|
||||||
```javascript
|
|
||||||
"use strict";
|
|
||||||
exports.__esModule = true;
|
|
||||||
var Hello = /** @class */ (function () {
|
|
||||||
function Hello() {
|
|
||||||
}
|
|
||||||
Hello.prototype.hello = function (msg) {
|
|
||||||
console.log(msg);
|
|
||||||
};
|
|
||||||
return Hello;
|
|
||||||
}());
|
|
||||||
exports.Hello = Hello;
|
|
||||||
```
|
|
||||||
就是一个语法的转换过程
|
|
||||||
|
|
||||||
### 语法与特性
|
|
||||||
ES6提供的语法糖就不提了, 基本每天都在写
|
|
||||||
就记录一下TS自己的一套类型体系吧
|
|
||||||
|
|
||||||
#### 变量类型
|
|
||||||
TS自带一套类型体系, 有一些与JavaScript当中有重叠 (当然JS当中也不能完全称之为类型, 因为都是对象)
|
|
||||||
string对应 JS的 String
|
|
||||||
number对应 JS的 Number
|
|
||||||
object对应 JS的 Object
|
|
||||||
boolean对应 JS的 Boolean
|
|
||||||
symbol对应 JS的 Symbol
|
|
||||||
|
|
||||||
所以定义变量时
|
|
||||||
```typescript
|
|
||||||
var str1: string = 'ab'
|
|
||||||
// 或者
|
|
||||||
var str2: String = 'cd'
|
|
||||||
```
|
|
||||||
都可以, 从eslint的默认设置来看, 官方推荐前者
|
|
||||||
> 变量类型定义同样可以应用于函数的形参, 语法相同
|
|
||||||
|
|
||||||
##### 数组
|
|
||||||
数组类型有两种表示方法
|
|
||||||
```typescript
|
|
||||||
var arr1: string[] = ['ab','cd']
|
|
||||||
var arr2: Array<string> = ['ab','cd']
|
|
||||||
```
|
|
||||||
后一种明显很像Java的泛型写法, 但是eslint默认的语法检查规则推荐前者
|
|
||||||
|
|
||||||
##### 枚举
|
|
||||||
```typescript
|
|
||||||
enum Color1 {Red, Green, Blue}
|
|
||||||
let c1: Color1 = Color1.Green
|
|
||||||
|
|
||||||
enum Color2 {Black = 10, White = 20}
|
|
||||||
let c2: Color2 = Color2.Black
|
|
||||||
```
|
|
||||||
使用枚举类型可以为一组数值赋予友好的名字
|
|
||||||
默认从0开始编号, 也可以手动指定编号
|
|
||||||
|
|
||||||
##### 任意值
|
|
||||||
我们并不是每次都能知道变量的类型是什么, 或者是引入的一些第三方库, 本身就是JS写的
|
|
||||||
没有类型声明
|
|
||||||
那么可以用`any`来直接忽略编译阶段的检查
|
|
||||||
|
|
||||||
> 类型检查是TS的最大优势, 主要是帮助开发者排除错误的
|
|
||||||
any只是一种兼容的做法, 不应该滥用, 否则和写JS还有什么区别
|
|
||||||
|
|
||||||
##### 空值
|
|
||||||
对于定义变量不算很常用, 有几种表示方法
|
|
||||||
```typescript
|
|
||||||
// void类型包含 undefined和null
|
|
||||||
let n1: void = undefined
|
|
||||||
let n2: void = null
|
|
||||||
|
|
||||||
// undefined和null也是类型
|
|
||||||
let n3: undefined = undefined
|
|
||||||
let n4: null = null
|
|
||||||
|
|
||||||
// 可以用void表示一个函数无返回值(当然JS当中无返回值就是返回undefined, 这点也正好符合)
|
|
||||||
function test(): void {
|
|
||||||
console.log('test')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### never
|
|
||||||
`never`可以用来表示函数的返回值, 表示用不存在的类型
|
|
||||||
该函数必须存在无法到达的终点 ( 比如抛出异常或者死循环 )
|
|
||||||
```typescript
|
|
||||||
function test1(): never {
|
|
||||||
throw new Error()
|
|
||||||
}
|
|
||||||
function test2(): never {
|
|
||||||
while(true) {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
#### 类型推断机制
|
|
||||||
|
|
||||||
如果声明变量时没有指定类型, 那么编译器就会根据初始化的赋值来推断这个变量的类型
|
|
||||||
对于函数返回值同样适用
|
|
||||||
比如
|
|
||||||
```typescript
|
|
||||||
let str = 'abc'
|
|
||||||
str = 10 // Error
|
|
||||||
```
|
|
||||||
初始化的赋值编译器推断str是string类型, 所以之后给他赋值number类型就会报错
|
|
||||||
|
|
||||||
#### 自定义类型
|
|
||||||
TS当中可以使用`interface`和`class`来自定义类型
|
|
||||||
与其他强类型语言当中的接口与类的定义类似, 前者可以写属性和方法声明, 后者可以实现接口
|
|
||||||
class定义的类型可以直接用new关键字来创建对象
|
|
||||||
```typescript
|
|
||||||
interface Demo {
|
|
||||||
name: string
|
|
||||||
value?: object
|
|
||||||
test(msg: string): void
|
|
||||||
}
|
|
||||||
let demo = {
|
|
||||||
name: 'sookie',
|
|
||||||
test: function(msg: string): void{
|
|
||||||
console.log(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// ? 代表该属性是可选的, 也可以应用于函数的形参
|
|
||||||
// 上述方式可以理解为给该接口创建一个实现类(类比Java当中的匿名类)
|
|
||||||
// 或者也可以用下面的方式实现一个接口
|
|
||||||
class DemoImpl implements Demo{
|
|
||||||
name: 'sookie'
|
|
||||||
test(msg: string): void{
|
|
||||||
console.log(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let demo2: Demo = new DemoImpl()
|
|
||||||
demo2.test('Hello')
|
|
||||||
```
|
|
||||||
需要注意的是, 继承接口需要定义函数的实现, 以及属性的值, 带有`?`的可选属性可以不指定
|
|
||||||
|
|
||||||
##### 访问控制
|
|
||||||
TS有3种访问控制关键字, 分别是`public`, `private`, `protected`
|
|
||||||
具体作用和Java一样, 缺省就是public
|
|
||||||
|
|
||||||
##### 构造器
|
|
||||||
可以对一个类编写一个构造器, 在对象被创建时调用
|
|
||||||
```typescript
|
|
||||||
class Demo {
|
|
||||||
constructor(private name: string) { }
|
|
||||||
show(): void {
|
|
||||||
console.log(this.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
new Demo('sookie').show()
|
|
||||||
```
|
|
||||||
构造器参数上添加任意一种访问控制关键字, 都可以直接把该参数设置为对象的属性
|
|
||||||
相当于构造器里面写**this.name = name**, 添加之后就可以省略这个赋值
|
|
||||||
|
|
||||||
|
|
||||||
### 写在最后
|
|
||||||
最近拿 [Nestjs](https://docs.nestjs.cn/) 给自己的博客写了个管理端的后台
|
|
||||||
当然前后端分离是必须的, Nestjs写后台也只写接口
|
|
||||||
这个框架需要用TypeScript编写代码, 于是就顺带学了学
|
|
||||||
|
|
||||||
据说习惯了TypeScript就再也不想写JavaScript了, 我倒是没怎么觉得
|
|
||||||
只是Nestjs的很多设计都和Spring有几分神似, 只能说Spring大法好, 上手倒是容易很多
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
---
|
|
||||||
title: TypeScript实用工具类型
|
|
||||||
date: 2021-05-11 15:22:09
|
|
||||||
tags:
|
|
||||||
- TypeScript
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
TypeScript当中有一些内置的类型,适用于编译过程
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
### Partial\<Type\>
|
|
||||||
将Type的所有属性都设置为可选的,表示输入类型的所有子类型
|
|
||||||
```typescript
|
|
||||||
interface Person {
|
|
||||||
name: string
|
|
||||||
code: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function show1(person: Partial<Person>) {
|
|
||||||
console.log(person)
|
|
||||||
}
|
|
||||||
|
|
||||||
function show2(person: Person) {
|
|
||||||
console.log(person)
|
|
||||||
}
|
|
||||||
|
|
||||||
show1({name: 'sookie'})
|
|
||||||
show2({name: 'sookie'}) // ERROR: Argument of type '{ name: string; }' is not assignable to parameter of type 'Person'.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Readonly\<Type\>
|
|
||||||
将Type的所有属性都设置为`readonly`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface Person {
|
|
||||||
name: string
|
|
||||||
code: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const person: Readonly<Person> = {
|
|
||||||
name: 'sookie',
|
|
||||||
code: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
person.code = 2 // ERROR: Cannot assign to 'code' because it is a read-only property.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Record\<Keys, Type\>
|
|
||||||
用来将某个类型的属性映射到另一个类型上
|
|
||||||
```typescript
|
|
||||||
interface PageInfo {
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
// 必须满足 string | number | symbol
|
|
||||||
type Page = 'home' | 'about' | 'contact'
|
|
||||||
|
|
||||||
const item: Record<Page, PageInfo> = {
|
|
||||||
about: { title: 'about' },
|
|
||||||
contact: { title: 'contact' },
|
|
||||||
home: { title: 'home' },
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pick\<Type, Keys\>
|
|
||||||
从类型Type中挑选部分属性Keys来构造类型
|
|
||||||
```typescript
|
|
||||||
interface Person {
|
|
||||||
name: string
|
|
||||||
age: number
|
|
||||||
remark: string
|
|
||||||
}
|
|
||||||
// 这里的第二个泛型值必须是属于 keyof Person
|
|
||||||
type PersonPick = Pick<Person, 'name' | 'age'>
|
|
||||||
|
|
||||||
const p: PersonPick = {
|
|
||||||
name: 'sookie',
|
|
||||||
age: 10
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Omit\<Type, Keys\>
|
|
||||||
与Pick用法类似,作用是相反的,用于从中剔除若干个key
|
|
||||||
```typescript
|
|
||||||
interface Person {
|
|
||||||
name: string
|
|
||||||
age: number
|
|
||||||
remark: string
|
|
||||||
}
|
|
||||||
type PersonOmit = Omit<Person, 'remark' >
|
|
||||||
|
|
||||||
const p: PersonOmit = {
|
|
||||||
name: 'sookie',
|
|
||||||
age: 10
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
---
|
|
||||||
title: TypeScript泛型
|
|
||||||
date: 2019-07-13 15:11:53
|
|
||||||
tags:
|
|
||||||
- TypeScript
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
大部分的编译型语言都有`泛型`
|
|
||||||
可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
### 泛型
|
|
||||||
在TS当中, 泛型可以用于方法也可以用于类
|
|
||||||
#### 用于函数的泛型
|
|
||||||
```typescript
|
|
||||||
function test<T>(arg: T):T {
|
|
||||||
return arg
|
|
||||||
}
|
|
||||||
let str1: string = test<string>('abc')
|
|
||||||
let str2: string = test('abc')
|
|
||||||
```
|
|
||||||
这种情况下调用该函数时就可以明确知道该函数的返回值类型与传入的第一个参数是相同的
|
|
||||||
上面的代码当中第二次调用函数时并未指定泛型, 这种情况下就是利用TS类型推断的机制, 根据第一个参数的类型来进行推断
|
|
||||||
|
|
||||||
#### 函数类型的表示方法
|
|
||||||
如果要为一个变量指定为函数类型, 由于函数包含多种要素, 有参数类型, 返回值类型, 还可能包含泛型
|
|
||||||
比如上面的test函数, 要用一个变量来接收这个函数的话就是
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
let t1: {<T>(arg: T): T} = test
|
|
||||||
// 也可以这样写
|
|
||||||
let t2: <U>(arg: U) => U = test
|
|
||||||
```
|
|
||||||
泛型的参数名并不要求必须一致
|
|
||||||
|
|
||||||
#### 用于类的泛型
|
|
||||||
```typescript
|
|
||||||
class Demo<T> {
|
|
||||||
value?: T
|
|
||||||
test(x: T, y: T): void {
|
|
||||||
console.log(x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let d = new Demo<string>()
|
|
||||||
```
|
|
||||||
> 类的静态成员不能使用泛型
|
|
||||||
|
|
||||||
#### 泛型约束
|
|
||||||
上面的方式写的泛型, 实际使用的时候相当于any, 也就是该泛型可以是任意类型
|
|
||||||
如果要约束这个泛型的类型范围
|
|
||||||
和Java当中一样, 也是使用`extends`关键字 ( 并没有super )
|
|
||||||
```typescript
|
|
||||||
class Demo1{
|
|
||||||
test(): void {
|
|
||||||
console.log('demo1')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Demo2<T extends Demo1> {
|
|
||||||
value?: T
|
|
||||||
}
|
|
||||||
```
|
|
||||||
> **注意** : 这里的`T extends Demo1`只是代表传入的泛型必须具备Demo1所有的成员
|
|
||||||
而不是必须是Demo1或者以extends Demo1来声明的类
|
|
||||||
比如有如下代码
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class Test {
|
|
||||||
// 这里的Test类具备一个名为test, 且无形参的函数成员(返回值不重要)
|
|
||||||
test(): void { console.log('Test') }
|
|
||||||
}
|
|
||||||
class Demo<T extends Test> { }
|
|
||||||
|
|
||||||
class Demo1 { }
|
|
||||||
class Demo2 {
|
|
||||||
test(str: string) { }
|
|
||||||
}
|
|
||||||
class Demo3 {
|
|
||||||
test() {}
|
|
||||||
}
|
|
||||||
let d1: Demo<Demo1> = new Demo() // ERROR: Demo1不具备test函数成员
|
|
||||||
let d2: Demo<Demo2> = new Demo() // ERROR: Demo2具备test函数成员, 但形参表不同
|
|
||||||
let d3: Demo<Demo3> = new Demo() // 正确
|
|
||||||
```
|
|
||||||
可以看到上述的几个类都没有继承自Test, 但是如果具备与Test相同的成员, 即可指定为Demo类的泛型
|
|
||||||
|
|
||||||
#### 查找类型
|
|
||||||
查找类型是 TS2.1 引入的新语法, 与之相关的关键字就是`keyof`和`type`
|
|
||||||
使用它获得的结果是一个**union**, 可以在泛型当中使用
|
|
||||||
(关于TS当中的union, 后面会提到)
|
|
||||||
```typescript
|
|
||||||
interface Demo {
|
|
||||||
name: string
|
|
||||||
value: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function test<T extends keyof Demo>(arg: T) { }
|
|
||||||
test('foo') // ERROR: Argument of type '"foo"' is not assignable to parameter of type '"name" | "value"'.
|
|
||||||
test('name') // 正确
|
|
||||||
|
|
||||||
type a = Demo['name'] // a就代表string类型
|
|
||||||
function test2(arg: a):void { }
|
|
||||||
//test2第一个参数必须是string类型
|
|
||||||
```
|
|
||||||
keyof 后面跟的是一个类型(可以是类或者接口)
|
|
||||||
返回的是这个类型当中所有成员的名称
|
|
||||||
|
|
||||||
使用type关键字可以获取一个类或者接口当中指定成员的类型
|
|
||||||
当然不只是适用于自定义的类
|
|
||||||
比如
|
|
||||||
```typescript
|
|
||||||
type b = string['charAt']
|
|
||||||
// 等价于
|
|
||||||
// type b = (pos: number) => string
|
|
||||||
```
|
|
||||||
### union
|
|
||||||
如果需要表示多种类型或值的组合, 可以使用`union`
|
|
||||||
它是一种**类型**, 可以用在任何需要使用类型的地方
|
|
||||||
这个类型中可以包含类型或者具体的对象
|
|
||||||
```typescript
|
|
||||||
type myType = string | 0
|
|
||||||
// 这种类型可以是任意字符串或者数字0
|
|
||||||
let a: myType = 'ab' // 正确
|
|
||||||
let b: myType = 0 // 正确
|
|
||||||
let c: myType = 1 // 错误, 只能是字符串或者数字0
|
|
||||||
|
|
||||||
function test(arg: number | 'none') {
|
|
||||||
// 该函数调用可以传入任意数字或者'none'字符串
|
|
||||||
console.log(arg)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 元组(Tuple)
|
|
||||||
第一次知道这种结构还是在python当中
|
|
||||||
当然原生JS没有元组了, TS实现了`元组(Tuple)`, 目的是合并不同类型的对象
|
|
||||||
而数组合并的是相同类型的对象
|
|
||||||
```typescript
|
|
||||||
let t: [string, number, Date] = ['123', 10, new Date()]
|
|
||||||
console.log(t[0].length)
|
|
||||||
```
|
|
||||||
其实完全可以把它看做一个数组, 实际的用法也和数组一样
|
|
||||||
|
|
||||||
### 枚举
|
|
||||||
TS也具备枚举类型, 与其他语言一样, 枚举可以用于清晰地表达意图或创建一组有区别的元素
|
|
||||||
比起直接使用数字表示, 代码更加清晰易读
|
|
||||||
```typescript
|
|
||||||
// 不指定数字, 从0开始的连续序列
|
|
||||||
enum Color1 {
|
|
||||||
RED, GREEN, BLUE
|
|
||||||
}
|
|
||||||
// 指定数字, 之后的依次递增
|
|
||||||
enum Color2 {
|
|
||||||
RED, GREEN=20, BLUE
|
|
||||||
}
|
|
||||||
// 指定初始化值, 则该成员就没有对应数字, 之后的必须也指定初始化值
|
|
||||||
enum Color3 {
|
|
||||||
RED, GREEN='green', BLUE='blue'
|
|
||||||
// 如果BLUE没有指定为'blue'就会报错
|
|
||||||
}
|
|
||||||
```
|
|
||||||
翻译后的JavaScript代码
|
|
||||||
```javascript
|
|
||||||
"use strict";
|
|
||||||
var Color1;
|
|
||||||
(function (Color1) {
|
|
||||||
Color1[Color1["RED"] = 0] = "RED";
|
|
||||||
Color1[Color1["GREEN"] = 1] = "GREEN";
|
|
||||||
Color1[Color1["BLUE"] = 2] = "BLUE";
|
|
||||||
})(Color1 || (Color1 = {}));
|
|
||||||
var Color2;
|
|
||||||
(function (Color2) {
|
|
||||||
Color2[Color2["RED"] = 0] = "RED";
|
|
||||||
Color2[Color2["GREEN"] = 20] = "GREEN";
|
|
||||||
Color2[Color2["BLUE"] = 21] = "BLUE";
|
|
||||||
})(Color2 || (Color2 = {}));
|
|
||||||
var Color3;
|
|
||||||
(function (Color3) {
|
|
||||||
Color3[Color3["RED"] = 0] = "RED";
|
|
||||||
Color3["GREEN"] = "green";
|
|
||||||
Color3["BLUE"] = "blue";
|
|
||||||
})(Color3 || (Color3 = {}));
|
|
||||||
```
|
|
||||||
可以看到它依然是用JS的键值对结构对象模拟实现的, 但是更多的时候我们不需要管这些
|
|
||||||
只需要按照熟知的枚举特点去使用
|
|
||||||
比如它是有固定数量实例的一种类型, 可以用于switch语句等等
|
|
||||||
|
|
||||||
@ -1,244 +0,0 @@
|
|||||||
---
|
|
||||||
title: Vue3新特性总结
|
|
||||||
date: 2022-03-13 00:43:37
|
|
||||||
tags:
|
|
||||||
- 前端
|
|
||||||
- Vue
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
Vue3发布已经有一段时间了, 各种特性和一些组件库的配套基本趋于成熟
|
|
||||||
这里就目前了解到的一些新特性和用法的变更做一些汇总
|
|
||||||
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
## 生命周期
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
在源码当中定义的生命周期钩子有如下这些
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
可以看到 beforeDestroy 已被标记为废弃, 建议使用`beforeUnmount`替代
|
|
||||||
destroyed 已被标记为废弃, 建议使用`unmounted`替代
|
|
||||||
`errorCaptured`是捕获一个来自后代组件的异常时被调用
|
|
||||||
|
|
||||||
## setup
|
|
||||||
在vue2当中, 使用的方式是`选项式API`
|
|
||||||
也就是在一个vue组件的定义当中, 可以包含 data、computed、methods 等等这些
|
|
||||||
为了减少代码迁移的成本, 这种方式在vue3当中依然支持, 可以不做重大改动
|
|
||||||
|
|
||||||
但是vue3提供了一个setup的选项, 可以在其中编写`组合式API`
|
|
||||||
|
|
||||||
这主要是源于使用选项式API, 如果一个组件当中包含诸多功能
|
|
||||||
就会出现这种情况
|
|
||||||

|
|
||||||
也就是同一个功能相关的代码, 会分布在各个指令当中
|
|
||||||
如果一个组件十分复杂, 代码就会变得难以维护
|
|
||||||
需要进行修改的时候, 就要到各个选项当中去找
|
|
||||||
|
|
||||||
> 如果有setup选项, 它的执行会**在beforeCreate之前**, 所以此时组件实例还未创建, 是不能使用this来表示组件实例的
|
|
||||||
|
|
||||||
```html
|
|
||||||
<template>
|
|
||||||
<div>{{obj}}</div>
|
|
||||||
<button @click="valueChange">valueChange</button>
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
```javascript
|
|
||||||
import { ref, reactive } from 'vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'App',
|
|
||||||
setup(props, context) {
|
|
||||||
const obj = ref({count:1, name:"123"})
|
|
||||||
const valueChange = () => {
|
|
||||||
obj.value.count = obj.value.count + 1
|
|
||||||
obj.value.name = "456"
|
|
||||||
}
|
|
||||||
return{
|
|
||||||
obj,
|
|
||||||
valueChange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
上述代码当中使用vue3提供的`ref`函数将一个对象转化为响应式对象
|
|
||||||
相当于之前在data当中的定义
|
|
||||||
而setup返回的对象中包含的函数, 相当于之前在methods当中定义的函数
|
|
||||||
|
|
||||||
### ref与reactive
|
|
||||||
|
|
||||||
与ref类似的还有`reactive`函数
|
|
||||||
+ reactive和ref都是用来定义响应式数据的,而reactive更推荐用来定义对象,ref更推荐定义基础数据类型,但是ref也可以定义对象
|
|
||||||
+ 在访问数据的时候,ref需要使用.value,而reactive不需要
|
|
||||||
|
|
||||||
### setup的参数
|
|
||||||
setup可以接受两个参数, 分别是`props`和`context`
|
|
||||||
+ props是组件传入的属性, vue2中使用props选项来定义的属性, 可以从这个参数当中获取到, 它本身就是响应式的
|
|
||||||
+ context可以理解为该组件的上下文, 提供了vue2的this中最常用的三个属性:attrs、slot 和emit
|
|
||||||
比如2当中的事件发射使用的是 this.$emit, 在3当中应当使用 context.emit
|
|
||||||
|
|
||||||
### 在setup中定义生命周期钩子函数
|
|
||||||
这里直接借用官方文档的表格了, 写的十分清晰
|
|
||||||
| 选项式 API | Hook inside setup |
|
|
||||||
| -- | -- |
|
|
||||||
| beforeCreate | Not needed* |
|
|
||||||
| created | Not needed* |
|
|
||||||
| beforeMount | onBeforeMount |
|
|
||||||
| mounted | onMounted |
|
|
||||||
| beforeUpdate | onBeforeUpdate |
|
|
||||||
| updated | onUpdated |
|
|
||||||
| beforeUnmount | onBeforeUnmount |
|
|
||||||
| unmounted | onUnmounted |
|
|
||||||
| errorCaptured | onErrorCaptured |
|
|
||||||
| renderTracked | onRenderTracked |
|
|
||||||
| renderTriggered | onRenderTriggered |
|
|
||||||
| activated | onActivated |
|
|
||||||
| deactivated | onDeactivated |
|
|
||||||
|
|
||||||
可以看到beforeCreate和created, 相当于是被包含在了setup当中
|
|
||||||
换句话说就是, 如果有某些需要在beforeCreate和created中执行的操作, 直接写在setup中即可
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup(props, context) {
|
|
||||||
onMounted(() => {
|
|
||||||
console.log('组件挂载完毕')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
### 解构赋值
|
|
||||||
对响应式对象直接进行解构赋值, 会破坏其响应式特性
|
|
||||||
也就是拿到的只是一个普通对象
|
|
||||||
```javascript
|
|
||||||
import { reactive } from 'vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup(props, context) {
|
|
||||||
const obj = reactive({count:1, name:"123"})
|
|
||||||
const valueChange = () => {
|
|
||||||
obj.count = obj.count + 1
|
|
||||||
obj.name = "456"
|
|
||||||
}
|
|
||||||
const { name } = obj
|
|
||||||
return{
|
|
||||||
obj,
|
|
||||||
name,
|
|
||||||
valueChange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
在上述代码当中, 在执行valueChange时, obj.name当然会改变, 但是name并不会改变
|
|
||||||
为此, vue3提供了`toRefs`可以来处理这种情况
|
|
||||||
```javascript
|
|
||||||
import { reactive, toRefs } from 'vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setup(props, context) {
|
|
||||||
const obj = reactive({count:1, name:"123"})
|
|
||||||
const valueChange = () => {
|
|
||||||
obj.count = obj.count + 1
|
|
||||||
obj.name = "456"
|
|
||||||
}
|
|
||||||
const { name } = toRefs(obj)
|
|
||||||
return{
|
|
||||||
obj,
|
|
||||||
name,
|
|
||||||
valueChange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
toRefs的作用就是把一个响应式对象, 转化为普通对象, 但是同时这个对象中所有属性的值变为响应式对象
|
|
||||||
此时对obj.name的改变, 就等同于对name的改变了
|
|
||||||
|
|
||||||
|
|
||||||
### 拆分
|
|
||||||
从上述的内容可以看得出来, 按照setup的用法
|
|
||||||
基本所有的业务代码都会在setup当中
|
|
||||||
这也会导致它变得十分冗长
|
|
||||||
但是它本身作为一个函数, 其中定义的都是这个函数当中的局部变量
|
|
||||||
这就提供了一种拆分的可能 (在vue2当中, 要对组件内的功能进行拆分, 可以使用mixin)
|
|
||||||
```javascript
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
const obj = ref({count:1, name:"123"})
|
|
||||||
const valueChange = () => {
|
|
||||||
obj.value.count = obj.value.count + 1
|
|
||||||
obj.value.name = "456"
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {obj, valueChange}
|
|
||||||
```
|
|
||||||
在组件当中直接这样使用即可
|
|
||||||
```javascript
|
|
||||||
import demo from './demo'
|
|
||||||
export default {
|
|
||||||
name: 'App',
|
|
||||||
setup() {
|
|
||||||
return{ ...demo }
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
### script setup
|
|
||||||
如果采用了setup, 其他的选项其实已经基本不再需要, 所以vue3支持以一种更加扁平化的方式来编写代码
|
|
||||||
也就是在script标签上添加setup属性
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import HelloWorld from './components/HelloWorld.vue'
|
|
||||||
|
|
||||||
const obj = ref({count:1, name:"123"})
|
|
||||||
const valueChange = () => {
|
|
||||||
obj.value.count = obj.value.count + 1
|
|
||||||
obj.value.name = "456"
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
此时script当中的所有代码, 就相当于之前setup当中的内容, 引入的组件也不再需要进行声明了, 直接使用即可
|
|
||||||
|
|
||||||
#### 属性与事件的声明
|
|
||||||
如果采用了`script setup`这种形式, 会发现没有办法像上面那样从setup函数的入参上获取props和context了
|
|
||||||
这种情况下我们可以使用`defineProps`和`defineEmits`
|
|
||||||
```html
|
|
||||||
<script setup>
|
|
||||||
const props = defineProps({msg: String})
|
|
||||||
const emits = defineEmits(['change', 'visible'])
|
|
||||||
|
|
||||||
emits('visible', '456')
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 关于sass
|
|
||||||
|
|
||||||
最后说一点题外话, 就是vue脚手架初始化创建项目时, CSS预处理器的选择
|
|
||||||
发现对于sass有两个选项, 分别是`node-sass`和`dart-sass`
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
大概去了解了一下
|
|
||||||
官方说法是这样的
|
|
||||||
> 此次改动是在 Sass 核心团队进行了大量讨论之后,得出的结论,现在是时候正式宣布弃用 LibSass 和基于它构建的包(包括 Node Sass)。多年来,LibSass 显然没有足够的工程带宽来跟上 Sass 语言的最新发展 (例如,最近的语言特性是在 2018 年 11 月添加的)。尽管我们非常希望看到这种情况有所改善,但即使 LibSass 长期贡献者 Michael Mifsud 和 Marcel Greter 的出色工作也无法跟上 CSS 和 Sass 语言开发的快速步伐。
|
|
||||||
|
|
||||||
主要包括以下四点说明
|
|
||||||
+ 不再建议将 LibSass 用于新的 Sass 项目, 改为使用 Dart Sass (opens new window)。
|
|
||||||
+ 建议所有现有的 LibSass 用户制定计划,最终迁移到 Dart Sass,并且所有 Sass 库都制定计划 最终放弃对 LibSass 的支持。
|
|
||||||
+ 不再计划向 LibSass 添加任何新功能,包括与新 CSS 功能的兼容性。
|
|
||||||
+ LibSass 和 Node Sass 将在尽力而为的基础上无限期维护,包括修复主要的错误和安全问题以及与最新的 Node 版本兼容。
|
|
||||||
|
|
||||||
|
|
||||||
概括来说就是官方推荐使用dart-sass, 也就是引入sass作为依赖
|
|
||||||
而不是使用node-sass
|
|
||||||
node-sass因为底层使用cpp编写, 安装依赖时需要编译cpp代码
|
|
||||||
在windows环境上有时会失败, 而且因为国情问题经常装不上
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,175 +0,0 @@
|
|||||||
---
|
|
||||||
title: Vue项目改造Typescript
|
|
||||||
date: 2020-4-21 09:07:33
|
|
||||||
tags:
|
|
||||||
- 前端
|
|
||||||
- TypeScript
|
|
||||||
categories:
|
|
||||||
- 前端杂烩
|
|
||||||
---
|
|
||||||
|
|
||||||
在Vue中使用TypeScript可以增强开发体验,主要是TS的类型检查可以在编译阶段检查出一些隐藏的编码错误
|
|
||||||
这是实践过程中将JavaScript编写的Vue项目改造为TypeScript的踩坑记录
|
|
||||||
<!-- more -->
|
|
||||||
|
|
||||||
### 添加依赖
|
|
||||||
最主要的是需要添加`typescript`、`ts-loader`的依赖
|
|
||||||
还可以添加上`vue-class-component`和`vue-property-decorator`用于增强
|
|
||||||
这两个库当中包含一些注解(装饰器),之前的Vue对象中的一些成员可以使用注解进行声明
|
|
||||||
后者相当于是前者的超集
|
|
||||||
|
|
||||||
### 打包配置修改
|
|
||||||
需要在`vue.config.js`当中添加webpack的配置
|
|
||||||
设置入口文件为main.ts文件,并且对于ts文件使用`ts-loader`进行处理
|
|
||||||
```javascript
|
|
||||||
module.exports = {
|
|
||||||
// ...省略其他配置
|
|
||||||
configureWebpack: { // 这里是webpack的配置
|
|
||||||
entry: './src/main.ts',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.ts$/,
|
|
||||||
loader: 'ts-loader',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
options: {
|
|
||||||
appendTsSuffixTo: [/\.vue$/],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
resolve: { // 这里是指定引入模块省略扩展名时的查找顺序
|
|
||||||
extensions: ['.ts', '.js', '.css', '.json', '.vue']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
### TypeScript配置
|
|
||||||
项目根目录下创建`tsconfig.json`文件
|
|
||||||
这里的配置可以根据实际需要创建
|
|
||||||
例如
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "esnext",
|
|
||||||
"module": "esnext",
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"importHelpers": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"src/*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"lib": [
|
|
||||||
"esnext",
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"scripthost"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts",
|
|
||||||
"src/**/*.tsx",
|
|
||||||
"src/**/*.vue",
|
|
||||||
"tests/**/*.ts",
|
|
||||||
"tests/**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
### main.ts
|
|
||||||
需要把原本的main.js文件改造为main.ts
|
|
||||||
内容的改造根据具体情况而定,主要是添加好符合ts语法的类型声明
|
|
||||||
|
|
||||||
除此之外,如果使用了vue-router和vuex,也需要改造为对应的ts文件
|
|
||||||
|
|
||||||
> 因为已经在webpack的配置当中添加了`extensions`的查找顺序
|
|
||||||
比如引入store.ts的时候就可以直接写`import store from './store'`
|
|
||||||
|
|
||||||
### 添加对Vue的声明
|
|
||||||
在src目录下创建文件`shims-vue.d.ts`
|
|
||||||
```typescript
|
|
||||||
declare module "*.vue" {
|
|
||||||
import Vue from "vue"
|
|
||||||
export default Vue
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
需要注意的是如果在Vue原型当中添加了属性
|
|
||||||
也需要添加对应的声明
|
|
||||||
但是由于vue-router和vuex这种是作为Vue插件来使用的
|
|
||||||
(也就是按`Vue.use(Router)`这种方式来使用)
|
|
||||||
并且都已经支持TypeScript,所以并不需要单独来编写声明了
|
|
||||||
|
|
||||||
但是像axios这种并不是按Vue插件使用
|
|
||||||
需要用下面的方式添加到Vue原型当中
|
|
||||||
```javascript
|
|
||||||
Vue.prototype.$http = axios
|
|
||||||
```
|
|
||||||
那么使用了TypeScript之后,就需要对该属性进行声明
|
|
||||||
否则ts编译的过程就会认为该属性不存在
|
|
||||||
|
|
||||||
在src目录下创建`index.d.ts`
|
|
||||||
```typescript
|
|
||||||
import { AxiosInstance } from 'axios'
|
|
||||||
|
|
||||||
declare module 'vue/types/vue' {
|
|
||||||
interface Vue {
|
|
||||||
$http: AxiosInstance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Vue组件改造
|
|
||||||
示例
|
|
||||||
```typescript
|
|
||||||
import { Component, Vue } from 'vue-property-decorator'
|
|
||||||
import { Button } from 'view-design'
|
|
||||||
@Component({components: { Button }})
|
|
||||||
export default class App extends Vue{
|
|
||||||
@Prop() formData?: object
|
|
||||||
msg: string | null = null
|
|
||||||
test() {
|
|
||||||
console.log(this.msg)
|
|
||||||
}
|
|
||||||
@Emit('reset')
|
|
||||||
resetCount() {
|
|
||||||
this.msg = null
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
相当于之前的写法
|
|
||||||
```javascript
|
|
||||||
import { Button } from 'view-design'
|
|
||||||
export default {
|
|
||||||
components: { Button },
|
|
||||||
props: {
|
|
||||||
formData: Object
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
msg: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
test() {
|
|
||||||
console.log(this.msg)
|
|
||||||
},
|
|
||||||
resetCount() {
|
|
||||||
this.msg = null
|
|
||||||
this.$emit('reset', 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
主要的效果就是组件代码的编写更加扁平化,减少了层级嵌套
|
|
||||||